DEV Community

Alan Norman
Alan Norman

Posted on

How to Lock Down Your Web App: Security Tips for Authentication – Alan Norman, Part 2

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' });
  } 
});
Enter fullscreen mode Exit fullscreen mode

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 */);
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)