How to Implement a Secure JWT Authentication and Registration with React and Node.js

How to Implement a Secure JWT Authentication and Registration with React and Node.js

Setting up secure user authentication can be a daunting task. But with the use of JSON Web Tokens (JWT) combined with React and Node.js, this process becomes more manageable and secure. Let’s break down how to do this.

Prerequisites

  • Basic knowledge of React, Node.js, and Express.
  • MongoDB with Mongoose ORM.
  • A familiarity with password hashing using bcrypt.Installing and Running MongoDB Locally:

1. Installation:

Windows:

  • Download the MongoDB installer from the official MongoDB website.
  • Run the downloaded .msi file and follow the installation wizard to install MongoDB.

MacOS (using Homebrew):

  • If you don’t have Homebrew installed, get it from here. Then:

brew tap mongodb/brew brew install [email protected]        

2. Starting MongoDB:

Windows:

  • Navigate to the directory where MongoDB is installed, usually something like C:\Program Files\MongoDB\Server\5.0\bin.
  • Run the following command:

mongod.exe --dbpath "C:\path\to\your\data\db"        

3. Accessing MongoDB:

  • Once the MongoDB service is running, you can interact with it using the mongo shell. Start it by running:

mongo        

4. Stopping MongoDB:

Windows:

  • Simply press Ctrl+C in the command prompt where mongod is running.

MacOS (using Homebrew):

brew services stop mongodb/brew/mongodb-community

1. Backend Setup:

First, let’s get our backend up and running.

Dependencies:

mkdir jwt-backend
cd jwt-backend
npm init -y
npm install express mongoose bcrypt jsonwebtoken cors        

User Schema (backend/models/user.js):

const mongoose = require("mongoose");
const bcrypt = require("bcrypt");

const userSchema = new mongoose.Schema({
  username: {
    type: String,
    required: true,
    unique: true,
    trim: true,
  },
  password: {
    type: String,
    required: true,
  },
  tokens: [
    {
      token: {
        type: String,
        required: true,
      },
    },
  ],
});

userSchema.methods.verifyPassword = async function (password) {
  const user = this;
  const isMatch = await bcrypt.compare(password, user.password);
  return isMatch;
};

const User = mongoose.model("User", userSchema);

module.exports = User;        

Setting up JWT (backend/utils/jwtHelper.js):

const jwt = require('jsonwebtoken');
const SECRET_KEY = "YOUR_SECRET_KEY";  // Store this securely!
const generateToken = (user) => {
    return jwt.sign({ id: user._id, email: user.email }, SECRET_KEY, {
        expiresIn: '1h'
    });
};
const verifyToken = (token) => {
    return jwt.verify(token, SECRET_KEY);
};
module.exports = { generateToken, verifyToken };        

Setting up Routes (backend/routes/auth.js):

const express = require("express");
const jwt = require("jsonwebtoken");
const User = require("../models/user");
const router = express.Router();
const bcrypt = require("bcrypt");

router.post("/login", async (req, res) => {
  const { username, password } = req.body;

  const user = await User.findOne({ username });

  if (!user) return res.status(400).send("Invalid username or password.");

  const validPassword = await bcrypt.compare(password, user.password);

  if (!validPassword)
    return res.status(400).send("Invalid username or password.");

  const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET);

  res.send({ token });
});

router.post("/register", async (req, res) => {
  try {
    const { username, password } = req.body;

    const existingUser = await User.findOne({ username });
    if (existingUser) {
      return res.status(400).json({ error: "Username already exists." });
    }

    const salt = await bcrypt.genSalt(10);
    const hashedPassword = await bcrypt.hash(password, salt);

    const user = new User({
      username,
      password: hashedPassword,
    });

    const savedUser = await user.save();
    res.json({
      message: "User registered successfully",
      userId: savedUser._id,
    });
  } catch (error) {
    console.error(error);
    res.status(500).json({ error: "Internal server error" });
  }
});

module.exports = router;        

Server Configuration (backend/server.js):

const express = require("express");
const mongoose = require("mongoose");
const authRoutes = require("./routes/auth");
const cors = require("cors"); // Import the CORS middleware
require("dotenv").config();

const app = express();
const PORT = 3001;

mongoose
  .connect(process.env.MONGODB_URI, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  })
  .then(() => {
    console.log("Connected to MongoDB");
  })
  .catch((err) => {
    console.error("Error connecting to MongoDB", err);
  });

app.use(cors()); // Use CORS middleware to allow requests from the frontend
app.use(express.json());
app.use("/api/auth", authRoutes); // All the routes defined in auth.js will be prefixed with /api/auth

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

Directory Structure:

backend/
|-- models/
|   |-- user.js
|-- utils/
|   |-- jwthelper.js
|-- routes/
|   |-- auth.js
|-- server.js
|-- package.json        

2. Frontend with React:

Setting Up React:

npx create-react-app frontend
cd jwt-frontend
npm install axios        

Directory Structure:

frontend/
|-- src/
|   |-- components/
|   |   |-- LoginForm.js
|   |   |-- RegistrationForm.js
|   |   |-- AuthContext.js
|   |   |-- Dashboard.js 
|   |-- App.js
|-- package.json        

LoginForm Component (frontend/components/Login.js):

import React, { useState, useContext } from "react";
import axios from "axios";
import { AuthContext } from "./AuthContext";
import { useNavigate } from "react-router-dom";

const Login = () => {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [errorMessage, setErrorMessage] = useState(null); // New state for handling error messages
  const { setToken } = useContext(AuthContext);
  const navigate = useNavigate();
  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      const response = await axios.post("/api/auth/login", {
        username,
        password,
      });
      setToken(response.data.token);
      localStorage.setItem("token", response.data.token);
      navigate("/dashboard");
    } catch (error) {
      console.error("Authentication failed:", error);
      setToken(null);
      localStorage.removeItem("token");
      if (error.response && error.response.data) {
        setErrorMessage(error.response.data); // Set the error message if present in the error response
      } else {
        setErrorMessage("An unexpected error occurred. Please try again.");
      }
    }
  };

  return (
    <div>
      {errorMessage && <div style={{ color: "red" }}>{errorMessage}</div>}{" "}
      <form onSubmit={handleSubmit}>
        <input
          value={username}
          onChange={(e) => setUsername(e.target.value)}
          placeholder="Username"
        />
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          placeholder="Password"
        />
        <button type="submit">Login</button>
      </form>
    </div>
  );
};

export default Login;        

Registration Component (frontend/components/Registration.js):

import React, { useState } from "react";
import axios from "axios";

const Registration = () => {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [message, setMessage] = useState("");

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      const response = await axios.post("/api/auth/register", {
        username,
        password,
      });
      setMessage(response.data.message);
    } catch (error) {
      console.error("Registration failed:", error.response.data.error);
      setMessage(error.response.data.error);
    }
  };

  return (
    <div>
      <h2>Register</h2>
      <form onSubmit={handleSubmit}>
        <input
          value={username}
          onChange={(e) => setUsername(e.target.value)}
          placeholder="Username"
          required
        />
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          placeholder="Password"
          required
        />
        <button type="submit">Register</button>
      </form>
      {message && <p>{message}</p>}
    </div>
  );
};

export default Registration;        

Authentication Context Component (frontend/components/AuthContext.js):

import React, { createContext, useState, useEffect } from "react";

export const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
  const [token, setToken] = useState(null);
  const [loading, setLoading] = useState(true); 

  useEffect(() => {
    const storedToken = localStorage.getItem("token");
    setToken(storedToken);
    setLoading(false); 
  }, []);

  return (
    <AuthContext.Provider value={{ token, setToken, loading }}>
      {children}
    </AuthContext.Provider>
  );
};        

Dashboard Protected Component (frontend/components/Dashboard.js):

import { useContext } from "react"; 
import { AuthContext } from "./AuthContext";
import { Navigate } from "react-router-dom"; 

function Dashboard() {
  const { token, loading } = useContext(AuthContext);
  if (loading) {
    return null;
  }

  if (!token) {
    return <Navigate to="/login" replace />;
  }

  return <h1>Dashboard: Protected Content Here</h1>;
}

export default Dashboard;        

3. Frontend Authentication Logic:

Authentication in the frontend typically revolves around managing a user’s session using tokens and controlling access to certain routes or views based on authentication status.

1. AuthContext.js:

This file defines the authentication context for your application.

  • AuthContext: This is a context object that lets React components access authentication-related data without having to pass props down through many levels. This is beneficial for managing global state like authentication.
  • AuthProvider Component: This component acts as a provider of the authentication context. It has the following key parts:
  • Local State: The component uses React’s useState to create a state for:
  • token: Represents the authentication token.
  • loading: Represents whether the token retrieval process is complete or not.
  • useEffect Hook: When the component first mounts, this effect checks localStorage for any stored authentication token. If found, it sets that token in the state. The loading state is then set to false to indicate that the token retrieval process is complete.
  • AuthContext.Provider: This wraps the children components and provides them with the value of the authentication context, which includes the token, a method to set the token, and the loading state.2. Login.js:This file defines the login component for your application.

  • username and password: These states manage the input values for the login form.
  • errorMessage: This state keeps track of any error messages to display during the login process.
  • useContext: The component uses this hook to access the setToken method from the AuthContext, allowing it to modify the global authentication token.
  • useNavigate: This hook provides a function for programmatic navigation, allowing the component to redirect the user upon successful login.
  • handleSubmit: This asynchronous function handles the login process:
  • It sends a POST request to the backend with the username and password.
  • On a successful login, it sets the received token in both the application context and localStorage. Then, it redirects the user to the dashboard.
  • If an error occurs, the token is cleared from both the application context and localStorage. An error message is displayed to the user.3. Dashboard.js:This file defines the dashboard component which is a protected route in your application.

  • useContext: The component uses this hook to access the token and loading values from the AuthContext.
  • Loading Condition: If the loading state is true, the component returns null, essentially rendering nothing until the token retrieval process is complete.
  • Token Condition: After loading completes, if there’s no valid token, the component redirects the user to the login page. If a token exists, it displays the protected dashboard content.
  • In summary, the authentication logic works by:

  1. Checking localStorage for any existing token and setting it in the application's global state.
  2. During login, validating user credentials, setting the received token, and handling errors.
  3. For protected routes like the dashboard, checking the presence of the authentication token and rendering content or redirecting based on its presence.Final Directory Structure (backend and frontend)

App/
|-- frontend/
|   |-- src/
|   |   |-- components/
|   |   |   |-- LoginForm.js
|   |   |   |-- RegistrationForm.js
|   |   |   |-- AuthContext.js
|   |   |   |-- Dashboard.js 
|   |   |-- App.js
|   |-- package.json
|-- backend/
|   |-- models/
|   |   |-- user.js
|   |-- utils/
|   |   |-- jwthelper.js
|   |-- routes/
|   |   |-- auth.js
|   |-- server.js
|   |-- package.json        

How to run the App?

1. Running MongoDB Locally:

Ensure MongoDB is running on your local machine. By default, MongoDB runs on localhost:27017. If you have MongoDB installed as a service, it may start automatically. If not, you can typically start it with:

mongod        

2. Backend Setup:

a. Navigate to your backend directory:

cd App/backend        

b. Install the required packages if you haven’t done so:

npm install        

c. Ensure your server.js (or equivalent entry point) is set up to connect to your local MongoDB. Look for a connection string similar to this:

mongoose.connect('mongodb://localhost:27017/yourdbname', { useNewUrlParser: true, useUnifiedTopology: true });        

Replace 'yourdbname' with the name of your database.

d. Run the backend:

npm start        

3. Frontend Setup:

a. Navigate to your frontend directory:

cd App/frontend        

b. Install required packages if you haven’t:

npm install        

c. Set up a proxy to your backend in the frontend’s package.json. This will make the React development server proxy any unknown requests to your backend server. Add the following line:

"proxy": "https://localhost:3001",        

Remember to replace 3001 with the port your backend is running on if it's different.

d. Run the frontend:

npm start        

By default, React apps start on localhost:3000.

4. Access the App:

You can now access your frontend app by going to:

https://localhost:3000/        

With this setup, any requests from your frontend that aren’t recognized as static assets (like your JavaScript or CSS) will be forwarded to your backend server. This is particularly useful during development to avoid CORS issues.

Follow me for such technical contents .


#JWTAuthentication #ReactNodeAuthentication #SecureAuthentication #ReactDevelopment #NodeJSDevelopment#BackendDevelopment

#FrontendDevelopment#WebDevelopment#MongoDB#AuthenticationTutorial

要查看或添加评论,请登录

Karan Rana的更多文章

  • 10 Must have VS Code Extensions for Developers in 2024

    10 Must have VS Code Extensions for Developers in 2024

  • Mastering Events and Forms in React

    Mastering Events and Forms in React

    Event Handling: Reacting to User Input Imagine your React app as a stage, where users interact with your meticulously…

  • CSS Tools for Enhanced Web Design

    CSS Tools for Enhanced Web Design

    "Here's a handpicked selection of CSS tools that you should absolutely explore:" In the vast ocean of web development…

  • Best Websites to Find Free React Templates

    Best Websites to Find Free React Templates

    HTMLrev HTMLrev (free) is an online gallery dedicated to presenting an extensive array of free templates for web…

    2 条评论
  • Hiring alert for 2024 Graduates

    Hiring alert for 2024 Graduates

    RedBus is hiring for the role of Tech Support Engineer! Expected salary: INR 6 - 12 LPA (via Glassdoor) Apply Link:…

  • Job Update

    Job Update

    Royal Enfield is hiring for the role of Lead Engineer! Expected salary: INR 7 - 12 LPA (via Glassdoor) Apply Link:…

  • Data Structures and Algorithms (DSA) is incredibly important for clearing interviews at top MNCs ??

    Data Structures and Algorithms (DSA) is incredibly important for clearing interviews at top MNCs ??

    Sharing some websites which would be really helpful in understanding of DSA concepts and improving coding skills. 1.

  • Job Update

    Job Update

    Ciena is hiring for the role of Technical Support Engineer! Expected salary: INR 5 - 10 LPA (via Glassdoor) Link:…

  • Magic of Kadane Algorithm ?

    Magic of Kadane Algorithm ?

    Let Understand step By Step Let's understand step By step: What is the Kadane algorithm ? Kadane's Algorithm, renowned…

    2 条评论
  • Django vs Mern Which one to choose?

    Django vs Mern Which one to choose?

    Django If you aim to develop a visually appealing website rapidly and effortlessly, Django is your go-to framework…

    4 条评论

社区洞察

其他会员也浏览了