Search code examples
javascriptnode.jsreactjsexpresscross-domain

HttpOnly cookies are not sent with request to server


I've created API in node express and I'm running it on port :8000, I am consuming APIs through simple CRA on port :3000. I've created registration and login with setting httpOnly cookie. Furthermore, I've put middleware to check each endpoint in order to verify if it has that token. When I test through Thunder/Postman everything works, after logging in I get the cookie in response, I set that cookie as auth token and make request to get data and I get the data.

When I log in through the React Frontend it succeeds and I can see in network tab that I have received the cookie in response. But when I make a request to protected endpoint, the request does not have a cookie in it (I log incoming requests on server and compare ones made with Thunder/Postman client and via app in Browser).

I use axios, and I've put {withCredentials: true} it doesn't work. I've used withAxios hook and it doesn't work either.

SERVER

index.js

...
const app = express()
app.use(cors({
    credentials: true,
    origin: 'http://localhost:3000',
}));
...

controllers/User.js

...
const loginUser = async(req, res) => {
    const body = req.body
    const user = await User.findOne({ email: body.email })
        if(user) {
        const token = generateToken(user)
        const userObject = {
            userId: user._id,
            userEmail: user.email,
            userRole: user.role
        }
        const validPassword = await bcrypt.compare(body.password, user.password)
        if(validPassword) {
            res.set('Access-Control-Allow-Origin', req.headers.origin);
            res.set('Access-Control-Allow-Credentials', 'true');
            res.set(
                'Access-Control-Expose-Headers',
                'date, etag, access-control-allow-origin, access-control-allow-credentials'
            )
            res.cookie('auth-token', token, {
            httpOnly: true,
            sameSite: 'strict'
            })
            res.status(200).json(userObject)
        } else {
            res.status(400).json({ error: "Invalid password" })
        }
    } else {
        res.status(401).json({ error: "User doesn't exist" })
    }
}
...

middleware.js

...
exports.verify = (req, res, next) => {
    const token = req.headers.authorization
    if(!token) res.status(403).json({ error: "please provide a token" })
    else {
        jwt.verify(token.split(" ")[1], tokenSecret, (err, value) => {
            if(err) res.status(500).json({error: "failed to authenticate token"})
            req.user = value.data
            next()
         })
    }
}
...

router.js

...
router.get('/bills', middleware.verify, getBills)

router.post('/login', loginUser)
...

CLIENT

src/components/LoginComponent.js

...
const loginUser = (e) => {
        setLoading(true)
        e.preventDefault()
        let payload = {email: email, password: password}
        axios.post('http://localhost:8000/login', payload).then(res => res.status === 200 
        ? (setLoading(false), navigate('/listbills')) : navigate('/register'))
    }
...

src/components/ListBills.js

...
useEffect(() => {
        fetch('http://localhost:8000/bills', {
            method: 'get',
            headers: {'Content-Type': 'application/json'}, 
            credentials: 'include',
        })
            .then(response => {console.log(response)}).catch(err => console.log(err));
    }, [])
...

I've also tried:

axios.get('http://localhost:8000/bills',{withCredentials: true})
  .then((data) => console.log(data))
  .then((result) => console.log(result))
  .catch((err) => console.log('[Control Error ] ', err))
    }

and

const [{ data, loading, error }, refetch] = useAxios(
  'http://localhost:8000/bills',{
  withCredentials: true,
  headers: {'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json'
}})

Console.log error:

enter image description here

After I login I get this in Network tab:

enter image description here

enter image description here

However when I want to access the list:

enter image description here

enter image description here

=== UPDATE ===

So the cause of the issue is not having the httpOnly cookie passed in the request header. This is the log of the middleware I am using:

token undefined
req headers auth undefined
req headers {
  host: 'localhost:8000',
  connection: 'keep-alive',
  'sec-ch-ua': '" Not;A Brand";v="99", "Google Chrome";v="97", "Chromium";v="97"',
  'sec-ch-ua-mobile': '?0',
  'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36',
  'sec-ch-ua-platform': '"macOS"',
  'content-type': 'application/json',
  accept: '*/*',
  origin: 'http://localhost:3000',
  'sec-fetch-site': 'same-site',
  'sec-fetch-mode': 'cors',
  'sec-fetch-dest': 'empty',
  referer: 'http://localhost:3000/',
  'accept-encoding': 'gzip, deflate, br',
  'accept-language': 'en-US,en;q=0.9,hr;q=0.8,sr;q=0.7,bs;q=0.6,de;q=0.5,fr;q=0.4,it;q=0.3'
}

token is read from headers.authorization but from the log of the headers it doesn't exist so my requests fail to be authorized.

Still not working.


Solution

  • After reading everything on CORS and httpOnly cookies I've managed to get it working. First I removed sameSite and added domain prop according to documentation in controllers/User.js on SERVER

    res.cookie('auth-token', token, {
        httpOnly: true,
        domain: 'http://localhost:3000'
    })
    

    Then I got a little yellow triangle in the console request view, it said that domain was invalid. Then I just changed domain to origin and the cookie appeared in the request log of the headers 🎉

    res.cookie('auth-token', token, {
        httpOnly: true,
        origin: 'http://localhost:3000',
    })
    

    The cookie was not in the Authorization property of the headers but in the cookie so I had to change the code in the middleware.js since it expected format bearer xxyyzz but receiving auth-token=xxyyzz, it looks like this now:

    exports.verify = (req, res, next) => {
        const token = req.headers.cookie
        if(!token) res.status(403).json({ error: "please provide a token" })
        else {
            jwt.verify(token.split("=")[1], tokenSecret, (err, value) => {
                if(err) res.status(500).json({error: "failed to authenticate token"})
                req.user = value.data
                next()
             })
        }
    }