I’m experiencing an issue where an HttpOnly cookie set by my backend is not appearing in the browser’s cookie storage. The Set-Cookie header is present in the server’s response, but the cookie value is undefined in the browser’s Application tab of the DevTools.
What I’m doing:
I’m developing a task management application with authentication handled via JWT tokens. The backend is using Express.js to set the token as an HttpOnly cookie, and the frontend is built with React and uses Axios for API requests.
Here is the relevant part of my backend code:
authController.js
async function login(req, res) {
const { email, password } = req.body;
try {
const user = await User.findOne({ email });
if (!user) {
console.log('User not found');
return res.status(400).json({ error: 'User not found' });
}
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
console.log('Invalid credentials');
return res.status(400).json({ error: 'Invalid credentials' });
}
const token = jwt.sign({ id: user._id }, secret, { expiresIn: '1h' });
console.log('Generated Token:', token);
res.cookie('token', token, {
httpOnly: true,
secure: false,
sameSite: 'Strict',
path:'/'
});
console.log('Set-Cookie Header:', res.get('Set-Cookie'));
res.status(200).json({ data: user });
} catch (error) {
console.error('Error logging in:', error);
res.status(500).json({ error: 'Error logging in' });
}
}
index.js:
import express from 'express';
import cors from 'cors';
import cookieParser from 'cookie-parser';
import router from './router.js';
const app = express();
const port = 8888;
const corsOptions = {
origin: 'http://localhost:5173',
credentials: true
};
app.use(express.json());
app.use(cookieParser());
app.use(cors(corsOptions));
app.use(router);
app.listen(port, () => {
console.log(`Server is listening on port ${port}`);
});
router.js:
import express from "express";
const router = express.Router();
import Task from "./controller/controller.js";
import Auth from "./controller/authController.js";
import authMiddleware from "./middleware/jwt.js"
router.get("/task", authMiddleware, Task.getTask);
router.post("/create", authMiddleware, Task.createTask);
router.delete("/delete/:id", authMiddleware, Task.deleteTask);
router.post('/signup', Auth.signUp);
router.post('/login', Auth.login);
router.post('/logout', authMiddleware, Auth.logout);
router.get('/user/me', authMiddleware, Auth.getCurrentUser);
export default router;
middleware:
import jwt from 'jsonwebtoken';
const secret = 'secret';
function authMiddleware(req, res, next) {
try {
const token = req.cookies.token;
if (!token) return res.status(401).json({ error: 'No token, authorization denied' });
const decoded = jwt.verify(token, secret);
req.user = decoded;
next();
} catch (e) {
console.log('Token is not valid', e);
res.status(400).json({ error: 'Token is not valid' });
}
}
export default authMiddleware;
frontend:
createPost:
import redaxios from 'redaxios';
export async function createPost(params: { title: string, body: string }) {
try {
const response = await redaxios.post("http://localhost:8888/create", params, {
withCredentials: true
});
return response.data.data;
} catch (e) {
console.log("problem in crud.tsx => createPost", e);
throw e;
}
}
sessions.tsx
import redaxios from 'redaxios';
import type { User } from '../index';
export async function login(params: {
email: string;
password: string;
}): Promise<User> {
const response = await redaxios.post("http://localhost:8888/login", params);
return response.data.data;
}
export async function logout() {
const response = await redaxios.post("http://localhost:8888/logout");
return response.data.data;
}
Form.tsx
import { FormEvent } from 'react';
import { createPost } from '../API/crud';
export default function Form() {
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
try {
const formData = new FormData(e.currentTarget);
const title = formData.get("title");
const body = formData.get("body");
if (typeof title === "string" && typeof body === "string") {
const result = await createPost({ title, body });
console.log("Result of calling post method", result);
return result;
} else {
console.log("Form fields aren't strings");
}
} catch (e) {
console.log("Handle submit error", e);
}
}
return (
<form
className='w-5/6 m-auto my-[2rem] flex flex-col bg-stone-500 p-[2rem]'
onSubmit={handleSubmit}
>
<div className='mb-[7%]'>
<label>Title</label>
<input name="title" className='text-zinc-700' />
</div>
<div className='mb-[7%]'>
<label>Body</label>
<input name="body" className='text-zinc-700' />
</div>
<button type="submit">Add Task</button>
</form>
);
}
I’ve spent 2 days trying to figure out what’s going on; Have tested all the backend with Postman and they all seem to work perfectly well. Login via client as well returns success and print via console.log actual token to servers terminal. Logging as well produces success in a Network tab of devtools, with set-Cookie header producing the desired value.
I know I’m doing something wrong on the client but i cannot quite pin what it is.
Any suggestions will be greatly appreciated