Building a Comprehensive User Authentication Flow with React and Node.js

Photo by Milo Rossi on Unsplash

Building a Comprehensive User Authentication Flow with React and Node.js

In this blog post, we will explore how to build a full-featured user authentication flow using React for the frontend and Node.js with MySQL for the backend. This guide will cover:

  • User Registration: Letting users sign up.

  • Login: Providing secure access for users.

  • Forgot Password: Assisting users who forget their passwords.

  • Reset Password: Allowing users to reset their passwords with a secure link.

  • Logout: Safely ending user sessions.

This project uses technologies like Express, MySQL, bcrypt for password hashing, and express-session for handling sessions. Let’s dive into each part, step by step.

Prerequisites

To follow along, you need:

  • Node.js and npm installed

  • React via Create React App

  • MySQL database setup

  • Basic knowledge of JavaScript, React, Node.js, and REST APIs

Backend Setup

Step 1: Setting Up Node.js Server

Create a new Node.js project:

mkdir mycrypt-backend
cd mycrypt-backend
npm init -y

Install the required dependencies:

npm install express mysql bcryptjs cors express-session dotenv crypto

Dependencies Overview:

  • Express: For the API server.

  • MySQL: To connect to the database.

  • Bcryptjs: For hashing passwords.

  • Cors: To handle cross-origin requests.

  • Express-session: To manage user sessions.

  • Crypto: To generate secure tokens for password reset.

Step 2: Create Database Connection File

Create a file named dbConnection.js to set up the database connection:

// dbConnection.js
const mysql = require('mysql');
require('dotenv').config();

const db = mysql.createConnection({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
});

db.connect((err) => {
  if (err) {
    console.error('Database connection failed:', err);
  } else {
    console.log('Connected to MySQL database');
  }
});

module.exports = db;

Add your database credentials to a .env file:

DB_HOST=localhost
DB_USER=root
DB_PASSWORD=your_password
DB_NAME=mycryptdb
PORT=5000
SESSION_SECRET=am_a_good_boy

Step 3: Backend Logic

Create a file named server.js to handle registration, login, forgot password, reset password, and session handling.

Here's the full backend code:

require('dotenv').config();
const express = require('express');
const db = require('./dbConnection');
const bcrypt = require('bcryptjs');
const cors = require('cors');
const session = require('express-session');
const crypto = require('crypto');

const app = express();
const port = process.env.PORT || 5000;

app.use(express.json());
app.use(cors({ origin: 'http://localhost:3000', credentials: true }));
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: { secure: false, maxAge: 3600000 },
}));

// Register Endpoint
app.post('/register', (req, res) => {
  const { email, password, confirmPassword } = req.body;
  if (!email || !password || !confirmPassword) {
    return res.status(400).json({ message: 'All fields are required' });
  }
  if (password !== confirmPassword) {
    return res.status(400).json({ message: 'Passwords do not match' });
  }

  db.query('SELECT * FROM users WHERE email = ?', [email], async (err, results) => {
    if (err) {
      return res.status(500).json({ message: 'Database error', error: err });
    }
    if (results.length > 0) {
      return res.status(400).json({ message: 'Email already exists' });
    }

    const hashedPassword = await bcrypt.hash(password, 10);
    db.query('INSERT INTO users (email, password) VALUES (?, ?)', [email, hashedPassword], (err, result) => {
      if (err) {
        return res.status(500).json({ message: 'Database insert error', error: err });
      }
      req.session.user = { id: result.insertId, email };
      res.status(201).json({ message: 'User registered successfully', user: { email } });
    });
  });
});

// Login Endpoint
app.post('/login', (req, res) => {
  const { email, password } = req.body;
  if (!email || !password) {
    return res.status(400).json({ message: 'All fields are required' });
  }

  db.query('SELECT * FROM users WHERE email = ?', [email], async (err, results) => {
    if (err) {
      return res.status(500).json({ message: 'Database error', error: err });
    }
    if (results.length === 0) {
      return res.status(400).json({ message: 'Invalid email or password' });
    }

    const user = results[0];
    const isMatch = await bcrypt.compare(password, user.password);
    if (!isMatch) {
      return res.status(400).json({ message: 'Invalid email or password' });
    }

    req.session.user = { id: user.id, email: user.email };
    res.status(200).json({ message: 'Login successful', user: { email: user.email } });
  });
});

// Forgot Password Endpoint
app.post('/forgot', (req, res) => {
  const { email } = req.body;
  if (!email) {
    return res.status(400).json({ message: 'Email is required' });
  }

  db.query('SELECT * FROM users WHERE email = ?', [email], (err, results) => {
    if (err) {
      return res.status(500).json({ message: 'Database error', error: err });
    }
    if (results.length === 0) {
      return res.status(400).json({ message: 'No account with that email found' });
    }

    const token = crypto.randomBytes(20).toString('hex');
    const expiryTime = Date.now() + 3600000; // 1 hour
    db.query('UPDATE users SET reset_password_token = ?, reset_password_expiry = ? WHERE id = ?', [token, expiryTime, results[0].id], (err) => {
      if (err) {
        return res.status(500).json({ message: 'Database error', error: err });
      }
      console.log(`Reset link: http://localhost:3000/reset/${token}`);
      res.status(200).json({ message: 'Password reset link sent to your email' });
    });
  });
});

// Reset Password Endpoint
app.post('/reset/:token', (req, res) => {
  const { token } = req.params;
  const { password, confirmPassword } = req.body;
  if (!password || !confirmPassword) {
    return res.status(400).json({ message: 'All fields are required' });
  }
  if (password !== confirmPassword) {
    return res.status(400).json({ message: 'Passwords do not match' });
  }

  db.query('SELECT * FROM users WHERE reset_password_token = ? AND reset_password_expiry > ?', [token, Date.now()], async (err, results) => {
    if (err) {
      return res.status(500).json({ message: 'Database error', error: err });
    }
    if (results.length === 0) {
      return res.status(400).json({ message: 'Invalid or expired token' });
    }

    const hashedPassword = await bcrypt.hash(password, 10);
    db.query('UPDATE users SET password = ?, reset_password_token = NULL, reset_password_expiry = NULL WHERE id = ?', [hashedPassword, results[0].id], (err) => {
      if (err) {
        return res.status(500).json({ message: 'Database error', error: err });
      }
      res.status(200).json({ message: 'Password reset successfully' });
    });
  });
});

// Logout Endpoint
app.post('/logout', (req, res) => {
  req.session.destroy((err) => {
    if (err) {
      return res.status(500).json({ message: 'Logout failed', error: err });
    }
    res.status(200).json({ message: 'Logout successful' });
  });
});

// User Endpoint
app.get('/user', (req, res) => {
  if (req.session.user) {
    res.status(200).json({ user: req.session.user });
  } else {
    res.status(401).json({ message: 'Unauthorized' });
  }
});

app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

Frontend Setup

1. Create React Application

Create a React project:

npx create-react-app mycrypt

2. Frontend Pages

  • Registration Page: Users can sign up by filling in their email, password, and confirmation password.

  • Login Page: Users can log in to their account.

  • Forgot Password Page: Users can enter their email to receive a password reset link.

  • Reset Password Page: Users can enter a new password by following the link sent to their email.

  • Wallet Page: Display the user's wallet and other features, including the logout option.

All these pages include form validation and feedback, making the flow seamless for the user.

Summary

In this tutorial, we learned how to build a full-featured authentication system using React, Node.js, and MySQL. We covered:

  • Setting up Node.js and MySQL.

  • Creating a REST API for user registration, login, forgot password, reset password, and logout.

  • Using bcrypt to hash passwords for security.

  • Using React for a dynamic frontend to interact with our backend server.

With these components, you have the building blocks for most authentication needs in modern web applications.

Feel free to expand on this example by adding other features such as email verification or OAuth integration to enhance the security and functionality of your app.