So recently I took on a challenge to learn advanced backend engineering all by myself. Quite a huge task, but I had to lock in.
Naturally, the time came when I was building a secure Authentication System with JWT Tokens — using Refresh and Access Tokens for session management.
Later, when I was studying common security attacks, I realized my own code would be pretty vulnerable in production. Here are the 4 bugs I found and exactly how I fixed them.
Bug 1 — SQL Injection
I was building query strings by directly embedding user input:
// ❌ What I was doing
const user = await pool.query(
`SELECT * FROM users WHERE id=${id}`
)
To understand the problem, you need to understand how query parameters travel from the browser to your backend.
A normal request looks like this:
GET /users?id=10
Which produces this SQL:
SELECT * FROM users WHERE id=10
Fine. But what if an attacker sends this instead?
GET /users?id=10 OR (1=1)
Your query becomes:
SELECT * FROM users WHERE id=10 OR (1=1)
Since 1=1 is always true — this returns every single user in your database. A complete data breach from one crafted URL.
It gets worse. An attacker can send:
GET /users?id=10; DROP TABLE users;--
Your entire users table — gone.
The fix — parameterized queries:
// ✅ The right way
const user = await pool.query(
'SELECT * FROM users WHERE id = $1',
[id]
)
The $1 placeholder tells PostgreSQL to treat the value as data, never as SQL. No matter what the user sends — it's always just a string. Injection is impossible.
Other options: input validation, ORMs like Prisma. But parameterized queries are the foundation — always use them.
Bug 2 — Email Enumeration
I was returning different error messages for wrong email vs wrong password:
// ❌ What I was doing
if (!user) {
return res.status(401).json({ message: 'Email not found' })
}
if (!passwordMatch) {
return res.status(401).json({ message: 'Wrong password' })
}
Looks harmless. It's not.
An attacker can write a script that fires thousands of emails at your login endpoint. If they get "Email not found" — they move on. If they get "Wrong password" — they know that email exists in your database.
Now they have a confirmed list of valid emails. They can target them with phishing, credential stuffing, or brute force attacks. You just handed them half the work.
The fix — always return the same message:
// ✅ The right way
return res.status(401).json({ message: 'Invalid credentials' })
Same message. Same HTTP status. No information leaked about whether the email exists or the password was wrong.
Bug 3 — Password Comparison with ===
I was comparing passwords like this:
// ❌ What I was doing
if (user.password === inputPassword) {
// login success
}
This is vulnerable to timing attacks.
String comparison in most languages stops the moment it finds a mismatching character. A password that matches the first 5 characters takes microseconds longer to reject than one that matches 0 characters.
An attacker can send thousands of password guesses and measure the response times. Over enough attempts, they can determine your password character by character — just from how long the server takes to respond.
The fix — use bcrypt.compare():
// ✅ The right way
const match = await bcrypt.compare(inputPassword, user.password)
bcrypt.compare uses a constant-time comparison algorithm — it always takes the same amount of time regardless of how many characters match. No timing signal. No leak.
And while we're here — never store plain text passwords. Always hash on signup:
const hashedPassword = await bcrypt.hash(password, 10)
// store hashedPassword in the database, never password
Bug 4 — Unrevocable Refresh Tokens
I was issuing refresh tokens as pure JWTs and forgetting about them:
// ❌ What I was doing
const refreshToken = jwt.sign(
{ userId: user.id },
process.env.REFRESH_TOKEN_SECRET,
{ expiresIn: '7d' }
)
res.json({ accessToken, refreshToken })
// that's it — no storage, no tracking
The problem — JWTs are stateless. Once issued, you cannot invalidate them before they expire.
If a user logs out, their refresh token is still cryptographically valid for 7 days. If it gets stolen — through XSS, a compromised device, a man-in-the-middle attack — the attacker has 7 days of access and you can't do anything about it.
Your logout endpoint becomes a lie. You tell the user they're logged out. They're not.
The fix — store refresh tokens in Redis:
// ✅ The right way — on login
const refreshToken = jwt.sign({ userId: user.id }, secret, { expiresIn: '7d' })
// Store in Redis with 7 day TTL
await redis.setex(
`refresh:${user.id}:${refreshToken}`,
7 * 24 * 60 * 60,
user.id.toString()
)
// On every /refresh request — check Redis first
const stored = await redis.get(`refresh:${decoded.userId}:${refreshToken}`)
if (!stored) {
return res.status(403).json({ message: 'Invalid or expired refresh token' })
}
// On logout — delete from Redis
await redis.del(`refresh:${decoded.userId}:${refreshToken}`)
// Token is now dead even if someone has a copy
Redis TTL handles expiry automatically. No cleanup jobs needed. And logout actually works.
The Takeaway
None of these bugs would show up in basic testing. SQL injection, email enumeration, timing attacks, unrevocable tokens — they only surface when someone is actively trying to break your system.
Reading the OWASP Top 10 after building your own auth system hits completely different than reading it before. You recognize every attack because you've written the vulnerable code yourself.
That's the only way to actually learn security — build it wrong first, then understand exactly why it's wrong, then fix it properly.
What I'm using in production:
- Parameterized queries with
pg(node-postgres) -
bcryptfor password hashing (cost factor 10) - Short-lived access tokens (15 minutes) + long-lived refresh tokens (7 days)
- Refresh tokens stored in Redis — revocable on logout
- Identical error messages for all auth failures
-
helmet.jsfor security headers - Rate limiting on auth routes (10 attempts per 15 minutes)
I'm documenting my backend learning journey publicly. If you found this useful, follow along — next up is Redis caching, Kafka event streaming, Docker, and system design.
(GitHub: )[https://github.com/Rick13211]
(X: )[https://x.com/theoblivied]
Top comments (0)