Hashing password in Next JS against rainbow table attacks with a salt for login page

If you want to send a username and password to the server side in nextJs and check if it is correct to continue the identification process.

In this article I will explain how and explain why it is important to add salt to a salad 😁

First I will explain why hashing passwords with a salt is an effective defense against rainbow table attacks. Here's why:

Rainbow Tables Explained: A rainbow table is a precomputed table for reversing cryptographic hash functions, primarily used for cracking password hashes. Suppose an attacker has a database of hashed passwords obtained from a compromised system. If these passwords were hashed without a salt, the attacker could use a rainbow table to look up the hash values and find the corresponding plaintext passwords. Rainbow tables are feasible because they can be precomputed for all possible plaintext passwords up to a certain length and complexity.

The Role of Salt: A salt is a random value that is added to the password before it is hashed. The salt is unique for each password; even if two users have the same password, their hashed passwords will be different because of the unique salts. When the password is stored, it is stored along with its salt.

Defeating Rainbow Tables: Since the salt is unique for each password, it effectively makes precomputed rainbow tables useless. An attacker cannot feasibly precompute a rainbow table for every possible salt; the number of possible salts (often 128-bit or more) makes the number of required rainbow tables astronomically high. For each unique salt, an entirely new rainbow table would need to be generated, requiring immense computational resources and storage.

Salting in Practice: When a password is salted and hashed, the salt is stored alongside the hash in the database. When a user attempts to log in, the system retrieves the salt for that user's password, adds it to the submitted password, and hashes the result. If the resulting hash matches the stored hash, the password is correct. This process ensures that even if two users have the same password, their stored hashes will be different due to the unique salts.

Additional Security with Work Factors: Modern hashing algorithms like bcrypt also incorporate a work factor (or cost factor), which is a measure of how computationally intensive the hashing process is. This slows down the hashing process deliberately, making brute-force attacks more time-consuming and difficult to execute.

In summary, salting passwords before hashing them protects against rainbow table attacks by ensuring that each password hash is unique, even if the original passwords are not. This uniqueness prevents attackers from using precomputed tables to reverse the hash values, significantly enhancing the security of stored passwords/

To securely hash passwords in a Next.js application, where the server-side is Node.js, you can use the bcrypt library. This process should happen on the server-side to ensure the plaintext password is never exposed. Here's a step-by-step guide:

Install bcrypt: First, you need to install bcrypt in your project. Run the following command in your terminal:

npm i bcrypt

Create an API Route for Registration: In your Next.js project, create an API route that will handle the registration process. This is where you'll hash the password.

Hash the Password: Inside your API route, use bcrypt to hash the password before saving it to your database.

Step 1: Create an API Route Create a file under pages/api/auth/register.ts. This will be your API endpoint for handling registration. Here's an example of how you might implement this:

// pages/api/auth/register.ts
import bcrypt from 'bcrypt';
import { NextApiRequest, NextApiResponse } from 'next';
const saltRounds = 10;
export default async function handler(req: NextApiRequest, res: NextApiResponse)  {
  if (req.method === 'POST') {
    try {
      // Extract the user data from the request body
      const { name, email, password } = req.body;

      // Hash the password
      const salt = await bcrypt.genSalt(saltRounds);
      const hashedPassword = await bcrypt.hash(password, salt);

      // Here, you would typically save the name, email, and hashedPassword to your database
      // For demonstration, we'll just return the hashed password
      return res.status(200).json({ name, email, hashedPassword });
    } catch (error) {
      return res.status(500).json({ error: 'Internal server error' });
    }
  } else {
    // Handle any non-POST requests
    res.setHeader('Allow', ['POST']);
    return res.status(405).end(`Method ${req.method} Not Allowed`);
  }
}

Implement the Registration Logic with Password Hashing ...

Step 2: Create Your Registration Page to Send Data to the API In your registration page component (RegisterPage), update the form submission logic to send a POST request to api/auth/register with the user's name, email, and password. You can use fetch for this:

// pages/auth/register/index.tsx
import Link from "next/link";
import { useState } from "react";

const RegisterPage = () => {
  const [registerData, setRegisterData] = useState({
    name: "",
    email: "",
    password: "",
  });

  const onChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
    setRegisterData({
      ...registerData,
      [e.target.name]: e.target.value,
    });
  };
  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {

    e.preventDefault();
    // Send the registerData state to the server
    const response = await fetch('/api/auth/register', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(registerData),
    });
  
    const data = await response.json();
    console.log(data); // For demonstration, log the response
  };

  return (
    <div>
      <h3>Register Page</h3>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="name">Name</label>
          <input
            onChange={onChange}
            value={registerData.name}
            type="text"
            name="name"
            required
          />
        </div>

        <div>
          <label htmlFor="email">Email</label>
          <input
            onChange={onChange}
            value={registerData.email}
            type="email"
            name="email"
            required
          />
        </div>

        <div>
          <label htmlFor="password">Password</label>
          <input
            onChange={onChange}
            value={registerData.password}
            type="password"
            name="password"
            required
          />
        </div>
        <button type="submit">Create account</button>
      </form>
      <div>
        Already have an account? <Link href="/auth/login">Login here</Link>
      </div>
    </div>
  );
};

export default RegisterPage;

Frontend (React) Securely Transmit Passwords: Ensure passwords are transmitted securely from the client to the server. Use HTTPS to encrypt the data in transit.

Minimal Password Handling: Avoid storing or logging the password in the client-side application unnecessarily.

Input Validation: Validate password inputs for length and complexity on the client side to improve user experience, but always validate on the server side as well for security.

Security Considerations HTTPS: Always use HTTPS to protect data in transit. Environment Variables: Store sensitive configuration (like saltRounds) in environment variables, not in your codebase. Database Security: Ensure your database is secured and accessible only by authorized users. CORS Policy: Configure CORS policy appropriately in your Node.js application to prevent unauthorized API access. By following these practices, you can significantly enhance the security of password storage and comparison in your application.

Step 3:  Create  a login page in Next Js. For this we need  to compere the password (that save at the DB with hash) and the email. We create an api for it - pages/api/auth/login.ts : 

// pages/api/auth/login.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import bcrypt from 'bcrypt';

// Dummy stored hash for demonstration. In a real app, fetch this from your database.
// Dummy function for demonstration. Replace with actual database query.
async function getUserByEmail(email: string) {
    // Simulate database access
    return {
      email: 'user@example.com',
      hashedPassword: '$2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92HgH/uPpizv.AlphaJG', // Replace with actual hash from your database
    };
  }
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === 'POST') {
    const { email, password } = req.body;

    try {

        const user = await getUserByEmail(email);
        if (!user) {
          // No user found with the submitted email
          return res.status(401).json({ message: 'Invalid credentials' });
        }

        // Comparing a submitted password with the stored hash
        // Assuming `password` is the password attempt from the user
        // and `hashedPassword` is the hashed password retrieved from your database
      const match = await bcrypt.compare(password,  user.hashedPassword);
      if (match) {
        // Email and passwords match
        res.status(200).json({ message: 'Login successful' });
      } else {
        // Passwords do not match
        res.status(401).json({ message: 'Invalid credentials' });
      }
    } catch (error) {
      res.status(500).json({ message: 'Server error', error: (error as Error).message });
    }
  } else {
    // Handle any other HTTP method
    res.setHeader('Allow', ['POST']);
    res.status(405).end(`Method ${req.method} Not Allowed`);
  }
}

Step 4: Create the react page for login

// pages/auth/login/index.tsx
import Link from "next/link";
import { useState } from "react";

const LoginPage = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = async (event: React.FormEvent) => {
    event.preventDefault();
    event.preventDefault();
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ email, password }),
      });

      if (!response.ok) {
        throw new Error('Network response was not ok');
      }

      const data = await response.json();
      alert(data.message); // Show success message
      // Handle further logic here (e.g., redirecting)
    } catch (error) {
      console.error('There was a problem with your fetch operation:', error);
    }
  };
  return (
    <div>
      <h3>Login Page</h3>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="email">Email</label>
          <input name="email" type="email" required />
        </div>

        <div>
          <label htmlFor="password">Password</label>
          <input name="password" type="password" required />
        </div>
        <button type="submit">Login</button>
      </form>
      <div>
        Do not have an account?{" "}
        <Link href="/auth/signup">Create an account</Link>
      </div>
    </div>
  );
};

export default LoginPage;

This code it for next 13. If you use next 14  I believe the the code is the same. You need to change index.tsx to page.txt and add "use client"; to the react pages.

Comments

Popular posts from this blog

A sharepoint list view of the current month

The specified path, file name, or both are too long. The fully qualified file name must be less than 260 characters

Export SharePoint 2010 List to Excel with PowerShell