Why Chrome can’t set cookie - node.js

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
);

Related

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

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.

Express JS/ Node JS : Browsers are not setting cookie when secure=true, sameSite: 'none'

it sets the cookie if I run the server locally, but when it is hosted online :
If secure=false, sameSite: 'none' then I get the following error
Cookie “connect.sid” will be soon rejected because it has the
“sameSite” attribute set to “none” or an invalid value, without the
“secure” attribute. To know more about the “sameSite“ attribute, read
https://developer.mozilla.org/docs/Web/HTTP/Headers/Set-Cookie/SameSite
then I tried with secure=true
if secure=true, sameSite: 'none' then I know it's supposed to work,
it works and the cookies are set when the server is hosted locally.
But when it is hosted in heroku the cookie are not set, and I get no
error.
It seems as if the client website is not secure but it shows https in the url box
What am I doing wrong here?
session config:
router.use(
session({
cookie: {
secure: true,
maxAge: 86400,
sameSite: "none",
},
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
})
);
note : I have already enabled cors with credentials set to true
The cookies tab was empty in the XHR cookies tab
Front-end and Back-end are hosted separately in heroku
XMLHttpRequest is used to send post request with withCredentials set to true.
XHRPOSThttps://sih-drs-prototype-backend-2.herokuapp.com/api/outrages/login
[HTTP/1.1 200 OK 1625ms]
POST
https://sih-drs-prototype-backend-2.herokuapp.com/api/outrages/login
Status200
OK
VersionHTTP/1.1
Transferred367 B (2 B size)
Access-Control-Allow-Credentials
true
Access-Control-Allow-Origin
https://tempautocomplete.herokuapp.com
Connection
keep-alive
Content-Length
2
Content-Type
application/json; charset=utf-8
Date
Sun, 12 Jul 2020 14:06:42 GMT
Etag
W/"2-vyGp6PvFo4RvsFtPoIWeCReyIC8"
Server
Cowboy
Vary
Origin
Via
1.1 vegur
X-Powered-By
Express
Accept
*/*
Accept-Encoding
gzip, deflate, br
Accept-Language
en-US,en;q=0.5
Connection
keep-alive
Content-Length
46
Content-Type
application/json;charset=UTF-8
Host
sih-drs-prototype-backend-2.herokuapp.com
Origin
https://tempautocomplete.herokuapp.com
Referer
https://tempautocomplete.herokuapp.com/static/
User-Agent
Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:78.0) Gecko/20100101 Firefox/78.0
The problem is not with expres-session, It does its job.
The browsers are not allowing cookies when the response comes from a 3rd party domain.
if you are looking for a workaround try this npm package :
should-send-same-site-none
https://www.npmjs.com/package/should-send-same-site-none
to be clear, the browser is not rejecting the cookies. Instead the cookies are stored in the name of the 3rd party domain name from which the response is sent.
It works perfectly fine when hosting locally since the request and the response would be from the same domain (localhost)
The settings, as posted in the original question are OK, only one thing was missing since Heroku might be using a proxy. I ran into the same problem and when I added:
app.set('trust proxy', 1);
The Set-Cookie header was finally sent from the Express server, hosted on Heroku to the browser.

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

CORS Header Missing on Angular Resource Requests Only

I have a working node/express backend running on localhost. I'm building a project application that needs to fetch data from goodreads api. When I execute the request, I get:
Cross-Origin Request Blocked:
The Same Origin Policy disallows reading the remote resource at
https://www.goodreads.com/book/title.json?author=Arthur+Conan+Doyle&key=[my_key]&title=Hound+of+the+Baskervilles.
(Reason: CORS header 'Access-Control-Allow-Origin' missing).1 <unknown>
Server side, everything is working correctly. I have enabled CORS, and when I check the headers, 'Access-Control-Allow-Origin' is available on everything coming from my server after checking the header in Firefox and Chrome dev tools. When I make a request via $resource, however, 'Allow-Access...' is not present in my header. Here is the code for the resource:
.factory('goodReads', function($resource) {
return $resource('https://www.goodreads.com/book/title.json');
})
.controller('AddBookSelectorController', function($resource, goodReads) {
this.fetch = function() {
var key = '[my_key]';
var data = goodReads.query({author: 'Arthur Conan Doyle', key: key, title: 'Hound of the Baskervilles'});
console.log(data);
};
});
I'm calling fetch via ng-click, and everything executes fine except I get the CORS error. Can anyone point me in the right direction? I am new to angular, and my suspicion is there is a problem with my resource request or something in configuration, but I can't seem to find an answer to fix my problem in the documentation or other stackoverflow questions.
Update 3: It is not a localhost issue. I tried pushing it to my domain and using a simple button which ran an xhr request to the OpenBooks api, and the problem got worse. It is hosted via Openshift, and now the 'Allow-Control-Access-x' headers are gone even for other files on my server. Really beginning to bang my head against the wall here. I am removing the Angular tags, because it has nothing to do with Angular.
UPDATE 2: I got it working after installing 'Allow-Control-Allow-Origin' extension in Chrome. Has my problem been the fact that I'm running this on localhost? Or is there something else going on? The header is still not being set without the extension.
UPDATE: I've been working on this since 8am, and still no luck. I have tried rewriting the request using Angular's $http and also with Javascript's xhr following the example from HTML5 Rocks | Using Cors and I'm still having the same problem with each method. Like I said, the necessary header information is available from files on my server, but it breaks when I make requests to other sites.
I'm starting to think this might not be an Angular problem, but I really have no clue. Just to be safe, here is the code I added to Express to enable CORS, including app.use so you can get an idea for where I called it:
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization, Content-Length");
res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
res.header("Access-Control-Allow-Credentials", "true");
next();
});
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', routes);
Edit: Here are the headers from the API request:
Request Headers
Host: www.goodreads.com
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:40.0) Gecko/20100101 Firefox/40.0
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
DNT: 1
Referer: http://localhost:3000/
Origin: http://localhost:3000
Connection: keep-alive
Response Headers
Cache-Control: max-age=0, private, must-revalidate
Content-Encoding: gzip
Content-Length: 686
Content-Type: application/json; charset=utf-8
Date: Wed, 02 Sep 2015 17:20:35 GMT
Etag: "a2be782f32638d2a435bbeaf4b01274a-gzip"
Server: Server
Set-Cookie: csid=BAhJIhg1MzgtNTk4NjMzNy0wNzQ4MTM5BjoGRVQ%3D--afed14b563e5a6eb7b3fa9005de3010474230702; path=/; expires=Sun, 02 Sep 2035 17:20:33 -0000
locale=en; path=/
_session_id2=fd45336b8ef86010d46c7d73adb5f004; path=/; expires=Wed, 02 Sep 2015 23:20:35 -0000; HttpOnly
Status: 200 OK
Vary: Accept-Encoding,User-Agent
X-Content-Type-Options: nosniff, nosniff
X-Frame-Options: ALLOWALL
X-Request-Id: 1K8EJWG30GWDE4MZ4R5K
X-Runtime: 2.277972
X-XSS-Protection: 1; mode=block
Headers for the .js file from my server:
Request
Host: localhost:3000
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:40.0) Gecko/20100101 Firefox/40.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
DNT: 1
Referer: http://localhost:3000/
Cookie: _ga=GA1.1.1924088292.1439681064; connect.sid=s%3AB4O0Up9WF5iqkfky__I0XCiBD2aMATlq.gbJUC9GseqnJvRTEIbcwxD6cwFQeL7ljNScURCJ5As0
Connection: keep-alive
If-Modified-Since: Wed, 02 Sep 2015 17:08:40 GMT
If-None-Match: W/"886-14f8f0828c1"
Cache-Control: max-age=0
Response:
Accept-Ranges: bytes
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=0
Connection: keep-alive
Date: Wed, 02 Sep 2015 17:20:30 GMT
Etag: W/"886-14f8f0828c1"
Last-Modified: Wed, 02 Sep 2015 17:08:40 GMT
X-Powered-By: Express
access-control-allow-headers: Origin, X-Requested-With, Content-Type, Accept
I guess this problem exposed my ignorance, but maybe this will help other newbies to CORs like me. I finally figured out the problem after getting a copy of CORs in Action and working through the first example using the Flickr API.
My problem had nothing to do with the backend, Angular, jQuery's .ajax method, or xhr. All of my requests were properly formatted. The problem was the APIs I attempted to use did not have CORs enabled on their server. O.o As soon as I changed the data type to jsonp, everything went through.
Anyway, for you newbs out there like me, here are some pointers to help you if you run into this problem:
1. Don't assume the API you are using has CORs enabled
I don't know why, but I blindly picked two APIs that don't have CORs enabled, which is what caused all the fuss for me. I have never run into this problem before because the work I have done with APIs have always been from big companies like Flickr that had CORs enabled. If they don't set Access-Control-Allow-Origin on their server, you can request them to enable it and use JSONP in the meantime.
If the API has an option for a callback at the end, that's a good sign you should use JSONP for your request. JSONP works by wrapping your request in a callback and exploiting a feature of the script tag. Scripts can pull other scripts from any domain, so it works as a hack to get the data. Here's a good link that helped me. Exactly What is JSONP? | CameronSpear.com
2. Check The Response Headers
I got tricked by this, but remember that the response header on your request to an external API is the response from their server, not yours. It doesn't matter if CORs is enabled on your server, you are making the request to someone else, and the browser automatically sends your information to them in the request header. Remember, all of this checking is done by the browser for security reasons, so its doing the heavy lifting for you on the request side based on your ajax call. If Access-Control-Whatever doesn't show up in the response header, they don't have CORs enabled. If you are working on the frontend and requesting someone else's data, you can't do anything about it. Use JSONP and your problems will disappear (probably).
This whole fiasco for me started because I was confusing responses coming from my server with responses coming for their server. I correctly enabled CORs on my own server, but I was thinking it wasn't attaching the origin information to the request header which is why it was absent in the response header. In reality, everything was working correctly, but the API server didn't have it enabled.
So a day spent, but many lessons learned. Hopefully my wasted time helps someone else with their CORs problems. Note that my issue was stack agnostic, so regardless of how you are making your request, checking the response header is the first course of action to take if you run into a problem with CORs. After that, I would suggest looking into the request itself for errors.
Check out that book above or this link from the same author for more help, especially when it comes to non-simple requests HTML5 Rocks | Using CORs.

Angular.js http GET request, CORS issue while sending cookies

I'm working on a project using node.js and express.js on server side, and angular.js on client side. I've set up authentication on server side using passport.js. So when I login the session cookie is set up and I can access the api via browser with no problem. But when I try to request the url of the api in angular.js with http, I get "cors blocked" (yeah, I made that up). Server allows credentials, client sends withCredentials: true. What is the problem?
Funny thing is server and client are not even in different domains. They are both in localhost. But since server "cors blocked" me in the very beginning, I've enabled cors.
So I have server and client in the same domain. Corse is enabled in server and I still can't send my cookies.
Response Headers
Access-Control-Allow-Orig... *
Connection keep-alive
Content-Length 33101
Content-Type application/json
Date Sun, 18 May 2014 15:02:41 GMT
Etag "-796401813"
X-Powered-By Express
access-control-allow-cred... true
Request Headers
Accept application/json, text/plain, */*
Accept-Encoding gzip, deflate
Accept-Language en-US,en;q=0.5
Cookie prgck=s%3AGb1ZXuD0uXHoBbFWIcrmkSFt.%2BNt0pdVa1%2BNhUNITAPOzPjmQxklPpDrdvQz%2BACj084o
Host localhost:1212
Origin http://localhost:8000
Referer http://localhost:8000/app/
User-Agent Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:29.0) Gecko/20100101 Firefox/29.0
It is not clear what you did on your Express server, but looking at the headers you posted it seems you enabled Access-Control-Allow-Credentials but you did not define the Access-Control-Allow-Origin.
This latter is necessary to actually enable CORS. So your Express server must set both:
Access-Control-Allow-Origin: http://my.domain.com:8080
Access-Control-Allow-Credentials: true;

Resources