reactjsexpresscookiesdeploymentsetcookie

Reactjs cient not receiving cookies from express server


I'm running my project on different servers, backend on render and frontend on vercel. However I'm only running into this error on deployment, meaning that the cookie is either not being set properly on the client side or not being retrieved correctly.

I've checked the credentials in cors and axios. I've also checked that the 'withCredentials' in res.cookies are set to true, as well as the other parameters. No errors in the console. Login shows that it is successful. It just doesn't receive cookies, so can't redirect to home. I don't know what else to do.

This is the Login function on the express server

const Login = async (req, res, next) => {
    try {
        const { email, password } = req.body
        if (!email || !password) {
            return res.json({ message: "All fields required" })
        }
        const user = await User.findOne({ email })
        if (!user) {
            return res.json({ message: "Incorrect email or password" })
        }
        const auth = await bcrypt.compare(password, user.password)
        if (!auth) {
            return res.json({ message: "Incorrect email or password" })
        }

        const token = createSecretToken(user._id);

        
        res.cookie("token", token, {
            withCredentials: true,
            httpOnly: false,
        });
        res.status(201).json({ message: "User logged in successfully", success: true });
        next()
    } catch (error) {
        console.log(error)
    }
}

This is the index.js on the server

const express = require('express');
const mongoose = require('mongoose');
const bcrypt = require('bcrypt')
const cookieParser = require("cookie-parser");
const bodyParser = require("body-parser")
const path = require('path')
const cors = require('cors')
require('dotenv').config();

const app = express();

const classRoutes = require('./routes/courseRoutes')
const authRoute = require("./routes/AuthRoutes");

const PORT = process.env.PORT || 4000;

mongoose.connection.on('connected', () => {
    console.log('Connected to MongoDB Atlas');
});

app.use((req, res, next) => {
    res.header("Access-Control-Allow-Origin", "https://divcourses.vercel.app");
    res.header("Access-Control-Allow-Credentials", "true");
    res.header("Access-Control-Allow-Methods", "GET,HEAD,OPTIONS,POST,PUT");
    res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization");
    next();
});

app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());

app.use('/api', classRoutes)
app.use('/api', authRoute);
app.use((req, res, next) => {
    console.log(`${req.method} ${req.url}`);
    next();
});
const atlasConnectionUri = process.env.MONGODB_URL;
mongoose.connect(atlasConnectionUri, {
    dbName: 'subjects'
});

app.get('/', async (req, res) => {
    try {
        res.status(200).json({ message: "Welcome to Home Route 🏠" })
    } catch (error) {
        res.status(500).json({ message: "Error in Home Route ❌" })
    }
});

app.listen(PORT, () => {
    console.log(`Server is Running at ${PORT}`);
});

This is the Login route on reactjs

import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import axios from "axios";
import { ToastContainer, toast } from "react-toastify";

const Login = () => {

    const navigate = useNavigate()
    const [inputValue, setInputValue] = useState({
        email: "",
        password: ""
    })

    const { email, password } = inputValue

    const handleOnChange = (e) => {
        const { name, value } = e.target
        setInputValue({
            ...inputValue,
            [name]: value
        })
    }

    const handleError = (err) =>
        toast.error(err, {
            position: "bottom-left",
        });
    const handleSuccess = (msg) =>
        toast.success(msg, {
            position: "bottom-left",
        });


    const handleSubmit = async (e) => {
        e.preventDefault()
        try {
            // `${baseURL}/login`
            const { data } = await axios.post(
                "https://mern-deploy-practice.onrender.com/api/login",
                { ...inputValue },
                { withCredentials: true }
            )
            console.log(data);
            const { success, message } = data;
            if (success) {
                handleSuccess(message);
                setTimeout(() => {
                    navigate("/");
                }, 1000);
            } else {
                handleError(message);
            }
        } catch (error) {
            console.log(error);
        }
        setInputValue({
            ...inputValue,
            email: "",
            password: "",
        });
    };

This is the Home route that the user should be redirected to after successful login

import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useCookies } from "react-cookie";
import { Link } from "react-router-dom";
import axios from "axios";
import { ToastContainer, toast } from "react-toastify";


const Home = () => {
    const [cookies, setCookies, removeCookie] = useCookies(['token']);
    const [username, setUsername] = useState('')
    const navigate = useNavigate()

    useEffect(() => {
                const verifyCookie = async () => {
            try {
                const response = await axios.post(
                    "https://mern-deploy-practice.onrender.com/api",
                    {},
                    { withCredentials: true }
                );

                if (response && response.data) {
                    const { status, user } = response.data;
                    setUsername(user)
                    setCookies('token', cookies.token)
                    const greeted = localStorage.getItem('greeted');
                    if (!greeted) {
                        toast(`Hello ${user}`, { position: "top-right" });
                        localStorage.setItem('greeted', 'true');
                    }
                } else {
                    console.error(error)
                    removeCookie("token");
                    navigate('/login');
                }
            } catch (error) {
                console.error("Error verifying cookie:", error);
                removeCookie("token");
                navigate('/login'); ``
            }

            if (!cookies.token) {
                navigate("/login");
                return;
            }
        };

        verifyCookie();
    }, [cookies, navigate, setCookies, removeCookie]);

    const Logout = () => {
        removeCookie("token");
        navigate("/signup");
    };

These are the response headers in the network tab
enter image description here

And these are the request headers
enter image description here


Solution

  • This is coming from confusion about the capabilities of cookies across domains.

    It looks like the back-end server lives at "https://mern-deploy-practice.onrender.com/api," and so when that sets the cookie, the browser stores that cookie against that domain.

    The front-end code is on the Vercel domain. Fundamentally, it can't read cookies set from calls to external domains because that cookie belongs to that external domain. You can make calls to that external domain though, and the browser will send the relevant cookies under the hood when you do implicitly. But you can only read/inspect cookies in the JS that are "owned" by the Vercel domain, and just because you called the external domain from this one doesn't mean it owns the cookie.

    If this were possible, it'd be a disaster because any website could potentially steal the cookies owned by another which would be a huge security flaw on the web platform. Hence, a fundamental principle is in place that the JS can only read cookies that belong to the same domain from which the JS itself was served.

    The solution here seems simple. Don't even try to check the cookie in JS. There appears to be no need unless I'm missing context. You know they are logged in by virtue of the auth HTTP req being successful. And you can rest assured that (even if you can't inspect or see them from the JS code) the browser will send the cookies to the external domain that was set by the back-end service that lives on that domain.

    If there's some data in there you need for whatever reason, typically the server would instead return this data on some API endpoint and you'd call that to get it.

    But if you, for some reason, really need this pattern, you could also try one of these techniques: