Set-Cookie Response Header not setting cross site cookie on Heroku - node.js

Locally, my cookies are set fine, but on Heroku with a cross-site call, they are not set in Chrome or Safari, the two browsers I've tried so far. I can only think that it is either because of the cross-site call or because of Heroku's proxy setup.
My Set-Cookie header looks like this:
CookieName=cookieValue; Max-Age=864; Domain=.myPurchasedDomain.net; Path=/; Expires=Tue, 21 Dec 2021 22:43:28 GMT; HttpOnly; Secure; SameSite=None
I have set my BE server to trust proxies, and both my FE and BE should only be available via https. I do not have any (visible) CORS issues. I am setting withCredentials: true in my Express BE and credentials: true in my FE axios options. I'm using next.js on my FE in case that might impact anything.
I don't know how to determine why my cookie is not being set. Any ideas of what I should try next or what might be causing the problem?
Edit: My FE axios call looks like this:
axios
.post(
"herokuUrl/login",
{
/* no body */
},
{
headers: {
Authorization: jwtToken, // Variable from elsewhere
},
withCredentials: true,
}
)

I found the answer:
My FE React app was calling a BE Express server at a different domain. Browsers block setting cookies from different domains. To fix, put both behind the same domain.

Related

Nextjs & custom Express server - cannot access cookie set with `http: true` in the express server [duplicate]

This question already has answers here:
How to send cookies with node-fetch?
(3 answers)
Why are cookies not sent to the server via getServerSideProps in Next.js?
(1 answer)
Closed 1 year ago.
I am trying to parse a cookie which was set by including the property httpOnly: true on my custom express server
Now in my NextJs app I can fetch the cookie in the server side method getServerSideProps and access the cookie in ctx.req.cookies from there. But when I make a fetch api call (from the server side method) to the custom server, the cookie does not seem to be accessible in the custom server api call.
Here is my express configuration in server.ts:
// Express Configuration
app.set('port', process.env.PORT || 3000)
app.use(cors({ credentials: true, origin: true }))
app.use(cookieParser())
app.use(passport.initialize())
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
This is how I set the cookie on custom express server:
res.cookie('rtk', refreshTokenJWT.token, {
httpOnly: true,
})
This is the fetch api call from getServerSideProps method:
const response = await fetch('http://localhost:3000/refresh_token', {
credentials: 'include',
method: 'POST',
})
Perhaps it is because of the fact that I have to make the api call with absolute url, when I try to do fetch('/refresh_token', ...) I get the error: TypeError: Only absolute URLs are supported
I could possibly send the payload from the server side method in the request body and handle it in the custom express server, but this does not seem like the best solution.
pls elp, thanks in advance.
Edit: To clarify, the await fetch() is happening in the server side method getServerSideProps of nextjs.
Sending the cookie as a header in the fetch api call as suggested by #O. Jones works, but I was under the impression that this wouldn't work since I had set httpOnly: true.
You have to, and are allowed to, parse that server response header despite the httponly attribute being present.
Why? Because HttpOnly (owasp link) is only an instruction to the browser that it should enforce that isolation; when your client code is just another node.js process, it's just a response header.
Here's the regurgitation I'm doing in my auth's unit testing that sounds very similar to your needs; I'm always getting exactly two cookies in comma-delimited one liner format, as emitted by my Koa RESTful app (your Express setup could be comma-delimited like this one, or send two Set-Cookies, so you may need a minor alteration if you have the latter case.)
// raw's format is `koa.sess=YA5N/yI1KhKc/qyylgNduj8vK3e2; path=/; expires=Sun, 09 May 2021 05:17:19 GMT; secure; httponly, koa.sess.sig=8x1BraqjAvKryLx1fvgc0DBu5D4; path=/; expires=Sun, 09 May 2021 05:17:19 GMT; secure; httponly`;
const scp = require('set-cookie-parser');
const cookies = scp.parse(scp.splitCookiesString(headers['set-cookie']));
const next_request_headers = {
cookie: cookies.map(cookie => cookie.name + '=' + cookie.value).join('; ')
};
I'm using npm:set-cookie-parser because cookies are jank and I got fed up with handling commas in both expirations and delimiters.
side issue: abs URLs
TypeError: Only absolute URLs are supported
Cookie domain rules are applied after relative URLs are normalized to absolute URLs, so it sounds to me like this side-issue is only about node-style fetches not having the implicit server/path needed for a browser-style relative ajax URL, and not blocking on anything to do with cookies.
getServerSideProps, by it's name, means it's generated server side
When you do a fetch in getServerSideProps, you do not have any default cookies.

Why Chrome can’t set cookie

I have a response from my server. Response Headers contains a valid Set-Cookie. Chrome Response/Cookies says that I have 1 cookie, ok. But... The cookie is not setting into DevTools/Application/Cookies
/*
My frontend on Vue
I renamed 127.0.0.1 to www.example.com
So running on www.example.com:80 or just www.example.com
*/
let response = await axios({
method: 'post',
url: 'http://127.0.0.1:3000/api/login', // my Node server on 3000 port
data: {
email : login,
password: password,
},
})
/*
My backend part on Node.js+Express
Running on localhost:3000 or 127.0.0.1:3000
CookieParser() is setted
*/
let options = {
maxAge: 345600000,
httpOnly: true,
path: '/',
}
res
.cookie('test', 'test', options)
.cookie('access_token', token, options) //token contains a JWT string
.send('');
/*
Chrome Response Headers
*/
Access-Control-Allow-Origin: *
Connection: keep-alive
Content-Length: 0
Content-Type: text/html; charset=utf-8
Date: Sun, 09 Feb 2020 08:52:22 GMT
ETag: W/"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"
Set-Cookie: test=test; Max-Age=345600; Path=/; Expires=Thu, 13 Feb 2020 08:52:22 GMT; HttpOnly
Set-Cookie: access_token=example; Max-Age=345600; Path=/; Expires=Thu, 13 Feb 2020 08:52:22 GMT; HttpOnly
X-Powered-By: Express
/*
Chrome request headers
*/
Accept: application/json, text/plain, '*/*'
Accept-Encoding: gzip, deflate, br
Accept-Language: ru-RU,ru;q=0.9
Cache-Control: no-cache
Connection: keep-alive
Content-Length: 45
Content-Type: application/json;charset=UTF-8
Host: 127.0.0.1:3000
Origin: http://www.example.com
Pragma: no-cache
Referer: http://www.example.com/
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36
Moreover I tried to use Microsoft Edge, but the problem isn't gone. Why browsers can't set-cookie considering they know about receiving Cookies?
If you want to do some background reading then I suggest you look for guides to CORS (cross-origin resource sharing) and, specifically, how to set cookies when using CORS.
Using your original code you'll need to change it to set withCredentials to true:
let response = await axios({
method: 'post',
url: 'http://127.0.0.1:3000/api/login', // my Node server on 3000 port
data: {
email : login,
password: password,
},
withCredentials: true
})
Behind the scenes this will set the withCredentials flag on XMLHttpRequest:
https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials
You'll then get CORS errors in your console. If you work your way through those errors you should be able to get it working. These errors will include...
Firstly, the server will need to return the header Access-Control-Allow-Credentials: true. This also applies to the preflight OPTIONS request.
Secondly, you're currently using Access-Control-Allow-Origin: * but that isn't allowed for requests using credentials. That'll need to be Access-Control-Allow-Origin: http://www.example.com. Likewise for the preflight.
At that point the cookie should be set in most browsers (more on browser quirks later).
To get the cookies added to subsequent requests you'll also need to set withCredentials: true on those requests. The other header changes outlined above also need to be applied. The cookies will not be included on the preflight request, just the main request.
Seeing the cookies in the browser's developer tools is not completely straightforward. Generally the tools only show cookies set for the same origin as the current page, which doesn't include the cookies set via CORS requests. To see those cookies you'll need to open another browser tab and set the URL to something that starts with the same origin as the CORS request. Be careful though, you need to pick a URL that won't set the cookies itself, otherwise that doesn't really prove anything.
Another way to check what cookies are set is to send another request (with withCredentials) and see what cookies get sent.
Then we have the browser-specific issues...
In Safari it is really difficult to get cookies to set over CORS. If Safari is a target browser for you then I suggest doing some further research into that.
Secondly, Chrome 80 is due to change the rules around CORS cookies significantly. See:
https://www.chromium.org/updates/same-site
You'll already see warnings about this in your console with older versions of Chrome. In short, CORS cookies will need to be served over https and set the directives Secure and SameSite=None.
Update:
Since writing this answer I have created an FAQ for common CORS problems. The section explaining the use of cookies can be found at https://cors-errors.info/faq#cdc8.
if you use Axios or Fetch - they don't send cookies by default.
if you go and check in the dav tools - (application tab -> cookies) you should see them.
it's not chromes problem but the way you trying to send them back, in fetch you need to add the credentials: 'include', to the option object, in axios it's {withCredentials: true}
if you are using express, might be use express-session for your session cookie.
from official documentation: https://www.npmjs.com/package/express-session
if you're using cross-site origin and https secure connection:
session: {
cookie: {
maxAge: 1000*60*60*1,
httpOnly: true, //set false if you want to change the cookie using JavaScipt
secure: true,
sameSite: 'none'
}
}
and don't forget to set:
app.set('trust proxy', 1) using express js
otherwise in localhost you can use
session: {
cookie: {
maxAge: 1000*60*60*1,
httpOnly: true, //set false if you want to change the cookie using JavaScipt
secure: false,
sameSite: 'lax'
}
}
Ran into the same problem like OP's, it turns out mine's pretty simple:
Need to pay attention when using axios, if you use this syntax:
axios.post()
What I did wrong:
axios.post(
"url",
{ withCredentials: true } // <- this is request body
);
Fix:
axios.post(
"url",
null,
{ withCredentials: true } // <- this is config
);

aiohttp not able to get all cookies from session

When using AIOHTTP to cookies of a session, it doesnt show the complete cookies.
I have tried to index through cookies, but it still just shows very few of them. I believe its using something called simple cookie but i could be wrong.
async with s.get(endpoint) as add:
print (endpoint.url)
sitetext = (await endpoint.text())
cookies = s.cookie_jar.filter_cookies(endpoint)
for each in cookies.items():
print (each)
Which yields results like:
('__cfduid', <Morsel: __cfduid=d2719c3095fb81ad69c19507dbe8bd7a01566353842>)
('_orig_referrer', <Morsel: _orig_referrer=>)
('_landing_page', <Morsel: _landing_page=productname>)
But this isn't the full cookies of the session, when i go and view the headers there are more cookies too, but im unsure of how to extract cookies from headers.
I tried getting cookies through headers by doing:
async with s.get(endpoint) as add:
print (endpoint.url)
sitetext = (await endpoint.text())
cookies = s.cookie_jar.filter_cookies(endpoint)
print (endpoint.headers['Set-Cookie'])
But this only gives me the first Set-Cookie and not the rest.
Edit: Im using AIOHTTP as this program has to be async and non blocking. (i know requests is much easier)
I am actually not sure, but I had similar problem. Try this:
cookies = response.cookies
cookie_string = "; ".join([str(value) for _, value in cookies.items()]).replace('Set-Cookie: ', '')
I have such format of answer:
__Host-ariregweb=g4q8icpEmmIdHnQmFSNsfHdNhLLsgISKE7XRzphrLiscb4pSIzrpRLm9aiLCHqXa; Domain=ariregister.rik.ee; HttpOnly; Path=/; SameSite=lax; Secure; __cf_bm=GIYOJNShc1kY79bPk7GG1U6T.jV6K4BG8DLQoc70NT8-1671428340-0-AcxNKFuuhvkmblvy/q4WPGEPezvLQUL8/k6NeylOmaX5awlf1L7eOWnc55DGMsyPzpv5YKUDL6w100KlzzjsJVE=; Domain=ariregister.rik.ee; expires=Mon, 19-Dec-22 06:09:00 GMT; HttpOnly; Path=/; SameSite=None; Secure; _cfuvid=0EIfdSb4ltpOgYzKuMthWVvN0x5L3kn_uCfhyvBEk34-1671428340155-0-604800000; Domain=ariregister.rik.ee; HttpOnly; Path=/; SameSite=None; Secure
As I see you try to bypass the CloudFlare protection. CloudFlare detects your scripted requests and gives 403 Forbidden status. So that you get non-completed cookies.
I recommend you to use Insomnia and Fiddler to understand what is happening with your request in browser and script. The only way to bypass the protection system is to define the key moments in headers or the character of request.
By the way, I have this problem currently. If you send the request through Fiddler in Python, you might be able to recieve completed cookies.
Good luck. But let me know if the solution was useful for your case.

Cookies disappear after redirect

I have:
1) A client side app that has its own domain: http://client.com
2) A server side app that has a separate domain: http://server.com
Now,
the scenario is:
1) Opening http://client.com/home in the browser, which displays an HTML page.
2) http://client.com/home redirects to http://server.com/login
3) http://server.com/login stores a cookie 'auth' and sends a redirect instruction to http://client.com/welcome
Response:
Access-Control-Allow-Origin: *
Connection: keep-alive
Content-Length: 104
Content-Type: text/html; charset=utf-8
Date: Wed, 16 Jan 2019 10:47:11 GMT
Location: http://client.com/welcome
Set-Cookie: auth=1479da80-197c-11e9-ba74-59606594e2fb; Path=/
Vary: Accept
X-Powered-By: Express
4) The browser receives the response, which does contain the cookie 'auth'
5) The browser redirects itself to http://client.com/welcome
6) 'auth' cookie is sent to http://client.com/welcome
Request:
Cookie: auth=1479da80-197c-11e9-ba74-59606594e2fb
7) http://client.com/welcome returns HTML but does not return the cookie 'auth'
8) http://client.com/welcome makes an AJAX request to http://server.com/data (CORS enabled), but the cookie 'auth' is not sent
9) http://server.com/data doesn't recognize the user because there is no cookie
The client side is an angular app hosted by Node.js
Edit:
As suggested, I've added to the response of server.com:
Access-Control-Allow-Credentials: true
but nothing has been changed.
Relevant client side code:
const headerOptions = new HttpHeaders({
'Content-Type': 'application/json', 'withCredentials': 'true', 'Access-Control-Allow-Origin': 'true', 'Access-Control-Allow-Credentials': 'true'
});
this.httpClient.get<any>(this.baseUrl + "data", { headers: headerOptions }).subscribe((res) => {
You should use the withCredentials option when sending your ajax request to your http://server.com and your server.com should have the Access-Control-Allow-Credentials set to true.
Example code in Node.JS server:
var cors = require('cors');
var corsOptions = {
origin: '*',
credentials: true };
app.use(cors(corsOptions));
More on this here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials
Example code in Angular.JS client
import {RequestOptions, Request, RequestMethod} from '#angular/http';
const options = new RequestOptions({
method: RequestMethod.Post,
url: 'https://google.com',
withCredentials: true
});
More on this here: https://angular.io/api/http/RequestOptions
Also check this out: https://github.com/angular/angular/issues/24283 - it looks like a particular version of Angular had problems with this flag, so unless you're using a more up-to-date version, you might need to set the header explicitly.
The reasoning of this is, unless the server explicitly tells the client "I will accept cookies (set previously on my domain) passed by another domain" - accepting the cookies would be a security issue. More here: https://en.wikipedia.org/wiki/Cross-site_request_forgery
Your description of what is happening does not seem right.
http://server.com/login stores a cookie 'auth' and sends a redirect instruction to http://client.com/welcome
'auth' cookie is sent to http://client.com/welcome
That is not (or at least should not be) what is happening. When the browser requests http://server.com/login and gets back in the response a Set-Cookie header, the cookie is set on and restricted to the server.com domain, even if the response is a redirect. If you are seeing the 'auth' cookie sent to client.com then that is a cookie that client.com previously set.
Anyway, it seems that what you really care about is
http://client.com/welcome makes an AJAX request to http://server.com/data (CORS enabled), but the cookie 'auth' is not sent
There are a bunch of reasons this can happen.
CORS. You mentioned it was CORS enabled, but for the sake of others reading this, you must have the following CORS headers set on server.com
Access-Control-Allow-Origin: http://client.com
Access-Control-Allow-Credentials: true
Note that you cannot get away with using a wildcard for Access-Control-Allow-Origin when you are sending credentials. Also note that the origin has to be an exact match, including scheme (http or https). In practice, what servers generally do is read the Origin header of the request, check it against a white list, and if it allowed, copy the Origin header value from the request to the Access-Control-Allow-Origin header in the response.
You must set xhr.withCredentials = true in your XHR request. (See MDN for more details.)
Then after you have done all that, you have one other hurdle in your way. Because you are on client.com and trying to send a cookie to server.com, the server.com cookie is considered a "third-party" cookie. AFAIK all the major browsers have a setting that blocks third-party cookies for privacy, because they are most often used by trackers to gather marketing data for advertising. I believe most of them block third-party cookies by default, but I am not sure of that. For sure lots of people have set their browsers to block third-party cookies.
So you have to tell your visitors to configure their browser to allow third-party cookies from server.com.
BTW, it is not safe to set a cookie on a redirect to a different domain. While it is allowed under the specification AFAIK, there have been issues with browser support. See, for example, this Chrome bug.
From Access-Control-Allow-Origin spec:
For requests without credentials, the literal value "" can be specified, as a wildcard;*
Try to add specific domain to Access-Control-Allow-Origin field.
I think you should use proxy in angular angular app. For more info check this link: https://github.com/angular/angular-cli/blob/master/docs/documentation/stories/proxy.md

Express js Redirect cookies not sent

I have 2 separate apps, let's call them Login & Dashboard. Both apps have a UI written in react and an express server.
In my Login app, when I make a POST from my Login UI, it hits the Login Express server to authenticate. Once authenticated, I set a cookie and redirect to my Dashboard url:
res.cookie(cookie.key, cookie.access_token, {
path: '/',
domain: cookie.domain,
httpOnly: true,
maxAge: cookie.rememberExpiry
})
res.redirect(dashboard_url)
However when I use req.cookies in my dashboard app I don't see any cookies.
When I make the POST from my Login UI I do indeed see a network call stating response header:
Set-Cookie: mycookie=cookievalue; Max-Age=28800; Domain=.local.myurl.com; Path=/; Expires=Thu, 03 Nov 2016 19:20:39 GMT; HttpOnly
Note that as of this moment the time is Nov 3 2016, 11:28 GMT so its not an expiry issue.
To test I have edited my hosts file such that login.local.myurl.com & dashboard.local.myurl.com point to localhost.
Is there any reason why the req.cookies is not available in the Dashboard express app??
My problem was 2 fold.
First I needed to set credentials: 'same-origin' on fetch, which is to say that I had to allow cookies to persist on the request library I was using.
Second, because my server and my client are essentially separate, a redirect on the server did not have the intended effect on the client. Hence I could not just res.redirect from the server response. instead I replaced the res.redirect line with res.status(200).send() and in my client code, I simply did window.location.replace('http://dashboardurl.com').
Hope that helps anyone who has this issue in the future.

Resources