I am working on user authentication for a website built using the MERN stack and I have decided to use JWT tokens stored as HttpOnly cookies. The cookie was sent in a “Set-Cookie” field in response header when I used Postman to make the request but not in the Safari Web Inspector as shown in the image below. There are no cookies found in the storage tab either.
I have simplified my React login form to a button that submits the username and password of the user for the sake of debugging
import React from "react"; const sendRequest = async (event) => { event.preventDefault(); let response; try { response = await fetch("http://localhost:5000/api/user/login", { method: "POST", body: { username: "Joshua", password: "qwerty" }, mode: "cors", // include cookies/ authorization headers credentials: "include", }); } catch (err) { console.log(err); } if (response) { const responseData = await response.json(); console.log(responseData); } }; const test = () => { return ( <div> <input type="button" onClick={sendRequest} value="send" /> </div> ); }; export default test;
I am using express on the backend and this is my index.js where all incoming requests are first received
const app = express(); app.use(bodyParser.json()); app.use("/images", express.static("images")); app.use((req, res, next) => { res.set({ "Access-Control-Allow-Origin": req.headers.origin, "Access-Control-Allow-Credentials": "true", "Access-Control-Allow-Headers": "Content-Type, *", "Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE", }); next(); }); app.use(cookieParser()); // api requests for user info/ login/signup app.use("/api/user", userRoutes);
This is the middleware that the login request is eventually directed to
const login = async (req, res, next) => { const { username, password } = req.body; let existingUser; let validCredentials; let userID; let accessToken; try { existingUser = await User.findOne({ username }); } catch (err) { return next(new DatabaseError(err.message)); } // if user cannot be found -> username is wrong if (!existingUser) { validCredentials = false; } else { let isValidPassword = false; try { isValidPassword = await bcrypt.compare(password, existingUser.password); } catch (err) { return next(new DatabaseError(err.message)); } // if password is wrong if (!isValidPassword) { validCredentials = false; } else { try { await existingUser.save(); } catch (err) { return next(new DatabaseError(err.message)); } userID = existingUser.id; validCredentials = true; accessToken = jwt.sign({ userID }, SECRET_JWT_HASH); res.cookie("access_token", accessToken, { maxAge: 3600, httpOnly: true, }); } } res.json({ validCredentials }); };
Extra information
In the login middleware, a validCredentials boolean is set and returned to the client. I was able to retrieve this value on the front end hence I do not think it is a CORS error. Furthermore, no errors were thrown and all other API requests on my web page that do not involve cookies work fine as well.
Another interesting thing is that despite using the same data (A JS object containing {username:”Joshua”, password:”qwerty”}) for both Postman and the React code, validCredentials evaluates to true in Postman and false in the Web Inspector. It is an existing document in my database and I would expect the value returned to be true, which was the case before I added cookies
May I know what I have done wrong or do you have any suggestions on how I can resolve this issue? I am a beginner at web-development
EDIT
With dave’s answer I can receive the “Set-Cookie” header on the frontend. However it does not appear in the Storage tab in the web inspector for some reason.
This is the response header
This is the Storage tab where cookies from the site usually appears
Advertisement
Answer
If you’re trying to send the request as json, you need to set the content type header, and JSON.stringify
the object:
response = await fetch("http://localhost:5000/api/user/login", { method: "POST", headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: "Joshua", password: "qwerty" }), mode: "cors", // include cookies/ authorization headers credentials: "include", });
Right now you’re probably getting the equivalent of
existingUser = User.findOne({ username: undefined})
and so when you do:
if (!existingUser) { validCredentials = false; } else { /* ... */ }
you get the validCredentials = false
block, and the cookie is set in the other block.