How to Lock Down Your Web App: Security Tips for Authentication – Alan Norman, Part 2
Alan Norman
Posted on September 18, 2024
Alright, let’s quickly recap what we covered in Part 1: User-Agent Validation, CORS, Rate Limiting, and CSP. Now, we’re diving a bit deeper into user authentication.
1. Kicking Off with CAPTCHA
Let’s start with something simple yet effective — CAPTCHA. Companies use different solutions like Google reCAPTCHA v2, v3, Cloudflare, or even roll their own.
We’ll focus on Google reCAPTCHA v3. So, what’s it all about? Unlike its older versions, reCAPTCHA v3 doesn’t throw annoying puzzles at users. Instead, it works in the background, scoring each visitor based on their behavior to figure out if they’re legit or a bot.
Implementing reCAPTCHA v3
Frontend: Pick your poison — whatever library fits your stack. Since I’m a React fan, I roll with @react-google-recaptcha. First, grab your Site Key and Secret Key by setting up reCAPTCHA here.
I won’t bog you down with frontend details, as there are tons of ways to implement it.
Backend: Here’s where things get straightforward:
app.post('/api/form', async (req, res) => {
const { token } = req.body;
try {
const response = await axios.post(`https://www.google.com/recaptcha/api/siteverify?secret=${RECAPTCHA_SECRET_KEY}&response=${token}`);
const { success, score } = response.data;
if (success && score > 0.5) {
// Token is valid, and score is good
res.json({ message: 'Success' });
} else {
// Token is invalid or score is too low
res.status(400).json({ message: 'Failed reCAPTCHA' });
}
} catch (error) {
console.error(error);
res.status(500).json({ message: 'Server error' });
}
});
Pro Tip: Be mindful of the response message. Don’t spill the beans on why the request failed — keep those error details vague to avoid giving attackers any clues.
2. Brute Force
Your system should recognize and prevent numerous failed login attempts from a single user. If someone is making repeated unsuccessful attempts, they’re likely trying to brute-force their way in.
Here’s how you can handle it:
Install and Configure Redis: Use Redis (a fast in-memory store) to track failed login attempts.
Implement Login Attempt Tracking:
Example of Controller:
const Login = async (req, res) => {
const { email, password } = req.body;
try {
const token = await LoginModule.login(email, password); // Try to log in
// Your logic here
} catch (e) {
// Handle login error
RedisService.setUserAttempts(`attempts_${email}`, /* increment and set TTL */);
}
}
Imagine you’ve got a LoginModule with a login method that does its thing and gives you a token if the login’s a success.
Meanwhile, RedisService has a setUserAttempts method that checks how many times a user’s tried logging in and adds one to the count.
Example of Middleware:
const USER_ATTEMPTS = 3; // Store this in env variables or system configuration
const UserLoginAttempts = (req, res, next) => {
const { body: { email } } = req;
const userAttempts = RedisService.getUserAttempts(`attempts_${email}`);
if (userAttempts >= USER_ATTEMPTS) {
return res.status(403).send('Too many attempts. Try again in 2 minutes.');
}
next();
}
Pro Tip: Make sure your Redis setup is fast and reliable. This is essential for managing login attempts effectively. Storing your login attempt configurations in Redis helps avoid additional database requests, which can slow down response times.
Alright, that’s a wrap for this step. Adding CAPTCHA and blocking brute force attacks will seriously beef up your app’s security.
Catch you in the next chapter where we’ll dive into Tokens and Cookies!
Posted on September 18, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 27, 2024