skip to Main Content

I am working on an app that makes API calls from react frontend to ExpressJs backend, everything works perfectly well on local development but is not working on the production.

I hosted my backend on Heroku and frontend on Netlify.

I can log in but can’t view my profile, when I try to view my profile it says that the token/cookies are undefined.

Here is my backend login

const loginUser = async (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }

  const { email, password } = req.body;

  try {
    // Check if the user exists
    const user = await User.findOne({ email });
    if (!user) {
      return res.status(400).json({ msg: "Invalid credentials" });
    }

    // Check if the password matches
    const isMatch = await bcrypt.compare(password, user.password);
    if (!isMatch) {
      return res.status(400).json({ msg: "Invalid credentials" });
    }

    // Create a payload with user ID and role (if applicable)
    const payload = {
      user: {
        id: user.id,
        role: user.role,
      },
    };

    // Sign a JWT token
    jwt.sign(
      payload,
      process.env.JWT_SECRET,
      { expiresIn: "2h" }, // Token valid for 2 hours
      (err, token) => {
        if (err) throw err;

        // Set the token as an HTTP-only cookie
        res.cookie("token", token, {
          httpOnly: true,
          secure: process.env.NODE_ENV === "production", // Use secure cookies in production
          sameSite: "strict", // Prevent CSRF attacks
          maxAge: 2 * 60 * 60 * 1000, // 2 hours in milliseconds
        });

        // Respond with user data and token
        res.status(200).json({
          user: {
            id: user.id,
            email: user.email,
            first_name: user.first_name,
            last_name: user.last_name,
            role: user.role,
          },
          token: token, // Include the token in the response body
        });
      }
    );
  } catch (err) {
    console.error(err.message);
    res.status(500).json({ msg: "Server error" });
  }
};

and the route

router.post(
  "/api/login",
  [
    check("email", "Please include a valid email").isEmail(),
    check("password", "Password is required").exists(),
  ],
  userController.loginUser
);

the frontend login

import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import axios from "axios";

const UserLoginForm = ({ setUser }) => {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState("");
  const navigate = useNavigate();

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      const response = await axios.post(
        "https://app.herokuapp.com/users/api/login",
        { email, password },
        { withCredentials: true }
      );
      setUser(response.data.user);
      console.log(response.data);
      localStorage.setItem("user", JSON.stringify(response.data.user));
      navigate("/");
    } catch (err) {
      setError(err.response?.data?.message || "An error occurred during login");
    }
  };

  return (
    <div className="min-h-screen bg-gray-100 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
      <div className="sm:mx-auto sm:w-full sm:max-w-md">
        <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
          Sign in to your account
        </h2>
      </div>

      <div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
        <div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
          <form className="space-y-6" onSubmit={handleSubmit}>
            <div>
              <label
                htmlFor="email"
                className="block text-sm font-medium text-gray-700"
              >
                Email address
              </label>
              <div className="mt-1">
                <input
                  id="email"
                  name="email"
                  type="email"
                  autoComplete="email"
                  required
                  value={email}
                  onChange={(e) => setEmail(e.target.value)}
                  className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
                />
              </div>
            </div>

            <div>
              <label
                htmlFor="password"
                className="block text-sm font-medium text-gray-700"
              >
                Password
              </label>
              <div className="mt-1">
                <input
                  id="password"
                  name="password"
                  type="password"
                  autoComplete="current-password"
                  required
                  value={password}
                  onChange={(e) => setPassword(e.target.value)}
                  className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
                />
              </div>
            </div>

            <div>
              <button
                type="submit"
                className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
              >
                Sign in
              </button>
            </div>
          </form>

          {error && (
            <div className="mt-4 text-center text-red-600">{error}</div>
          )}
        </div>
      </div>
    </div>
  );
};

export default UserLoginForm;

the middleware

const jwt = require("jsonwebtoken");
const User = require("../models/User");
const Salon = require("../models/Salon");

const authMiddleware = async (req, res, next) => {
  console.log(req.cookies, "this is cookies");
  try {
    // Get token from cookie
    const token = req.cookies.token || req.header("x-auth-token");

    // Check if no token
    if (!token) {
      req.user = null;
      req.salon = null;
      res.locals.authenticatedEntityId = null;
      return next();
    }

    // Verify token
    const decoded = jwt.verify(token, process.env.JWT_SECRET);

    if (decoded.user) {
      // If the token contains user information
      const user = await User.findById(decoded.user.id).select("-password");

      if (!user) {
        throw new Error("User not found");
      }

      req.user = user;
      req.salon = null;
      res.locals.authenticatedEntityId = user._id;
    } else if (decoded.salon) {
      // If the token contains salon information
      const salon = await Salon.findById(decoded.salon.id).select("-password");

      if (!salon) {
        throw new Error("Salon not found");
      }

      req.salon = salon;
      req.user = null;
      res.locals.authenticatedEntityId = salon._id;
    } else {
      // If the token doesn't contain user or salon information
      req.user = null;
      req.salon = null;
      res.locals.authenticatedEntityId = null;
    }

    next();
  } catch (error) {
    console.error("Auth Middleware Error:", error);
    req.user = null;
    req.salon = null;
    res.locals.authenticatedEntityId = null;
    next();
  }
};

module.exports = authMiddleware;

the middleware is returning undefined to the token

When I login it shows the token in cookies, but if I click any link or refresh the page the token will disappear, so I can’t view my profile or do anything that requires me to use the user.

Remember that it works perfectly on local development but not working on production.

My Api is hosted on Heroku and frontend on netlify

2

Answers


  1. When your frontend and backend are hosted on different domains (like Netlify for the frontend and Heroku for the backend), you might encounter issues with cross-origin requests due to the sameSite attribute in cookies. If you set sameSite: "strict", the browser may block the cookie from being sent between these domains, which could interfere with authentication or session persistence.

    To resolve this, you should use sameSite: "none" instead of "strict". However, when using sameSite: "none", the cookie must also be marked as secure, meaning it will only be sent over HTTPS connections.

    Login or Signup to reply.
  2. When developing locally the same-origin policy doesn’t apply in the exact same way as it does in production with cross-origin resource sharing(CORS).

    Since your frontend and backend are hosted on different domains you might need to set the SameSite attribute of the cookie to None to allow it to be sent in cross-origin requests.

    Change the code below:

    // Set the token as an HTTP-only cookie
            res.cookie("token", token, {
              httpOnly: true,
              secure: process.env.NODE_ENV === "production", // Use secure cookies in production
              sameSite: "strict", // Prevent CSRF attacks
              maxAge: 2 * 60 * 60 * 1000, // 2 hours in milliseconds
            });
    

    To this code :

    // Set the token as an HTTP-only cookie
                res.cookie("token", token, {
                  httpOnly: true,
                  secure: process.env.NODE_ENV === "production", // Use secure cookies in production
                  sameSite: "none", // allows the cookie to be sent cross-origin
                  maxAge: 2 * 60 * 60 * 1000, // 2 hours in milliseconds
                });
    

    This allows your cookies to be sent cross-origin which in this case is Heroku sending the cookies to Netlify.

    Also update your backend entry file to add the CORS middleware and specify what URL you want to make the Cross-origin request to. For example:

    const cors = require('cors');
    app.use(cors({
      origin: 'https://your-netlify-app.netlify.app', // Frontend URL
      credentials: true,  // Allow credentials to be sent (cookies)
    }));
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search