1

I know this question has been asked a bunch of times, but I tried most of the answers and still can't get it to work.

I have a Golang API with net/http package and a JS frontend. I have a function

func SetCookie(w *http.ResponseWriter, email string) string {
    val := uuid.NewString()
    http.SetCookie(*w, &http.Cookie{
        Name:     "goCookie",
        Value:    val,
        Path:     "/",
    })
    return val
}

This function is called when the user logs in, and I expect it to be sent to all the other endpoints. This works as expected with Postman. However, when it comes to the browser, I can't seem to get it to remember the cookie or even send it to other endpoints.

An example of JS using an endpoint

async function getDataWithQuery(query, schema){
    
    let raw = `{"query":"${query}", "schema":"${schema}"}`;
    let requestOptions = {
        method: 'POST',
        body: raw,
        redirect: 'follow',
    };
    try{
        let dataJson = await fetch("http://localhost:8080/query/", requestOptions)
        data = await dataJson.json();
    }catch(error){
        console.log(error);
    }

    return data;
}

I tried answers like setting SameSite attribute in Golang, or using credential: "include" in JS with no luck.

sideshowbarker
  • 81,827
  • 26
  • 193
  • 197
Mohamed Yasser
  • 641
  • 7
  • 17
  • Sounds for me not as a `Go` issue but one on your client's side. Does `fetch()` check for the `SetCookie` header and honor it, i.e. set the cookie? – NotX Dec 29 '22 at 15:27
  • In the js authentication, I use `localStorage.setItem("cookie", res.cookie)`. I tried using `document.cookie`, which sets only the value and ignores the name, secure, path, ...etc. Even when I put it manually with `document.cookie` and set its path to `/` I still can't get the browser to send it automatically. – Mohamed Yasser Dec 29 '22 at 16:04
  • @MohamedYasser How do you exercise the endpoint that's meant to set the cookie? Does it involve a cross-origin request? – jub0bs Dec 29 '22 at 17:33
  • I couldn't do it when both where on `localhost`, I've been tinkering a bit more and managed to make the browser send it when both are on `localhost`. My guess is that `localStorage` for some reason didn't see it, so I switched to writing the cookie by hand using `document.cookie = \`goCookie=${res.cookie}; path=/; domain=localhost\`` . It works with both on localhost. Switching to `127.0.0.1` instead of localhost causes some CORS errors. I set the API headers to allow the origin `127.0.0.1:5500` (js origin) and now the cookie isn't being sent again. – Mohamed Yasser Dec 29 '22 at 17:49
  • I would like to open another question regarding the CORS problem, since this question was originally about storing the cookie. But before doing so, I would like to note that the problem with storing the cookie was with `localStorage`. `document.cookie` saved it successfully. For the browser sending the cookies, the `requestOptions` require the field `credentails:'include'` field. My mistake was using `credential` instead of `credentials`. Now the problem is, I don't want to set the cookie by hand using the `document.cookie` I want to set the cookie returned from the API. – Mohamed Yasser Dec 29 '22 at 17:55
  • 1
    If you want to send a cross-origin request with cookies, you need to specify `credentials: 'include'` in the `fetch` options. See https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#sending_a_request_with_credentials_included – jub0bs Dec 29 '22 at 20:05
  • Thanks for leading me toward the solution. This question was meant for a single problem of many, but I solved all of them. You can look at the answer that worked for me below. – Mohamed Yasser Dec 29 '22 at 20:34

1 Answers1

1

Thanks to the discussion in the comments, I found some hints about the problem.

Saving cookies (both API and frontend on the same host)

I used document.cookie to save the cookie. I set the options by hand since calling res.cookie on the response of the API fetch only returned the value. An example is document.cookie = `goCookie=${res.cookie}; path=/; domain=localhost;.

Sending cookies

This has been answered before in previous questions and answered again in the comments. The problem was that I used credential:'include' instead of the correct credentials:'include' (plural).

CORS and cookies

In case the API and the frontend are not on the same host you will have to modify both the API and the frontend.

frontend

The cookie has to have the domain of the API since it's the API that requires it, not the frontend. So, for security reasons, you can't set a cookie for a domain (API) from another domain (frontend). A solution would be redirect the user to an API endpoint that returns Set-Cookie header in the response header. This solution signals the browser to register that cookie with the domain attached to it (the API's domain, since the API sent it).

Also, you still need to include credentials:'include' in the frontend.

API

You will need to set a few headers. The ones I set are

    w.Header().Set("Access-Control-Allow-Origin", frontendOrigin)
    w.Header().Set("Access-Control-Allow-Credentials", "true")
    w.Header().Set("Access-Control-Allow-Headers", "Content-Type, withCredentials")
    w.Header().Set("Access-Control-Allow-Methods", method) // use the endpoint's method: POST, GET, OPTIONS

You need to expose the endpoint where the frontend will redirect the user and set the cookie in the response. Instead of setting the domain of the API by hand, you can omit it, the browser will fill it with the domain automatically.

To handle the CORS and let JS send the cookie successfully, you will have to set the SameSite=None and Secure attributes in the cookie and serve the API over https (I used ngrok to make it simple).

Like so

func SetCookie(w *http.ResponseWriter, email string) string {
    val := uuid.NewString()
    http.SetCookie(*w, &http.Cookie{
        Name:     "goCookie",
        Value:    val,
        SameSite: http.SameSiteNoneMode,
        Secure:   true,
        Path:     "/",
    })
   // rest of the code
}

I recommend you also read the difference between using localStorage and document.cookie, it was one of the problems I had.

Hope this helps.

Mohamed Yasser
  • 641
  • 7
  • 17
  • I think "Also, for security reasons, cookies are only sent over secure connections" is wrong. For example, in go you can [skip the certifcate check](https://stackoverflow.com/a/12122718/5767484) (for the backend) and I'ld be suprised if it doesn't work then. – NotX Dec 30 '22 at 10:55
  • I am not a JS expert, but I read the `credentials:'include'` only works for when the cookie has attribute `SameSite=None`, which in turn requires the cookie to be `secure`. I'll try to test again with the link you provided when I'm available. – Mohamed Yasser Dec 30 '22 at 12:46
  • Okay, then I misunderstood your sentence. I got it as: You can't set _any_ cookies over http. – NotX Dec 30 '22 at 13:06
  • 1
    Oh! My bad, will add clarification to the answer. – Mohamed Yasser Dec 30 '22 at 13:29