I'm having issues with cookies in a MERN app hosted to Vercel (Front-end) and Heroku (Back-end).
Everything is working fine in localhost, but when deployed i'm having issues storing cookies. Set-Cookie is sent with the sign in request, and the request itself looks fine. I get no errors.
After being signed in, authenticated routes return what I expect for each user, so the cookie seems to be accessible to the back-end even tho the cookie is not stored on the front-end under storage, which I assume is a security issue. This is not the case in Safari, in Safari the cookies are gone after login, so the user is logged out again.
The only route that does not work is signing out. On sign out I clear the token cookie, but it is trying to delete something that technically isn't there, still no errors. UPDATE: Clearing cookie works! I wasn't sending any data back with the sign out, so the front-end thought it was unresponsive, adding a .send({ message: 'Sign Out Successful' }) solved that issue. However, still no cookie visible in the front-end storage.
For CORS settings I have the origin set to the front-end url, and credentials set to true. While on the front-end I have withCredentials: true set on every request.
When deployed, the cookie is using sameSite: none, and secure: true.
Below are the sign in and sign out routes, but you can find the full back-end code here, and the front-end code here on GitHub.
Sign in route on back-end
import type Route from '../../types/Route'
import jwt from 'jsonwebtoken'
import env from '../../env/env'
import bcrypt from 'bcryptjs'
import User from '../../models/User'
import validateSignIn from '../../utils/validation/signIn'
const route: Route = {
method: 'post',
execute: async (req, res) => {
const { username, password } = req.body.user
try {
const { errors, valid } = validateSignIn(username, password)
if (!valid) return res.status(401).send({ errors })
const user = await User.findOne({ username })
if (!user) return res.status(404).send({ errors: { username: 'User not found' }})
const correctPassword = await bcrypt.compare(password, user.password)
if (!correctPassword) return res.status(401).send({ errors: { password: 'Wrong password' } })
const token = jwt.sign({ userId: user._id }, env.SECRET, { expiresIn: "1hr" })
return res.status(200).cookie('token', token, {
expires: new Date(Date.now() + 604800000),
secure: env.ENVIRONMENT === 'LIVE',
sameSite: env.ENVIRONMENT === 'LIVE' ? 'none' : 'lax',
httpOnly: true
}).send(user)
} catch (error) {
console.log('@sign/in', error)
return res.sendStatus(500)
}
}
}
export default route
Sign out route on back-end:
import type Route from '../../types/Route'
import authorization from '../../middlewares/http'
import env from '../../env/env'
// TODO: Fix Sign Out
const route: Route = {
method: 'get',
authorization,
execute: async (req, res) => {
try {
return res.clearCookie('token').sendStatus(200)
} catch (error) {
console.log('@sign/out', error)
return res.status(500)
}
}
}
export default route