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
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.
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 toNone
to allow it to be sent in cross-origin requests.Change the code below:
To this code :
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: