Cookies disappear after redirect - node.js

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

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.

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.

Cookies not being passed from response to next request

I'm trying to build my first Web app and I am using cookies for authentication.
Basically, my React client sends the auth credentials to my REST API which verifies it and generates a token if the credentials are valid.
When I inspect the network activity on chrome, the token is set on the response header of the /login POST request, but when I try to access a protected route after this, I get a 401 and inspecting the request reveals that the cookie was not present on the header.
How do I combat this? I thought this was a CORS issue at first but I just cannot seem to solve it.
Response of the login call:
Cookie missing on the next request:
How I've handled the CORS issue:
app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", "http://localhost:8080");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Set-Cookie");
next();
});
Thanks in advance.
Your next POST request to protected route doesn't contain the cookie, because by default it doesn't send the cookies.
If you're using axios.
set withCredentials: true
axios.get(`api_url`, { withCredentials: true })
By setting { withCredentials: true } you may encounter cross-origin issue. To solve that you need to use
expressApp.use(cors({ credentials: true, origin: "http://localhost:8080" })); //Your REACT ADDRESS
Read more about withCredentials here https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials
The issue must be because of Set-cookie attribute SameSite=strict. SameSite attribute works in following way.
SameSite= samesite-value
Strict: The browser sends the cookie only for same-site requests (that is, requests originating from the same site that set the cookie). If the request originated from a different URL than the current one, no cookies with the SameSite=Strict attribute are sent.
Lax: The cookie is withheld on cross-site subrequests, such as calls to load images or frames, but is sent when a user navigates to the URL from an external site, such as by following a link.
None: The browser sends the cookie with both cross-site and same-site requests.
In your case subsequent request is also cross origin so browser doesn't attach cookie in the header. It should work if SameSite=None

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

Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Reactjs

I'm deploying my web app to heroku and use the following cors config on the client side.
const instance = axios.default.create({
baseURL: "https://myapp-backend.herokuapp.com",
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
"Access-Control-Allow-Origin": "https://myapp-backend.herokuapp.com"
}
});
And on the server side
const corsConfig = {
origin: ['https://myapp.herokuapp.com', 'http://localhost:3001']
}
app.use(cors(corsConfig));
But I keep getting error: "Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource."
Any help would be much appreciated. Sometimes the config works but sometimes it doesn't...
The custom headers on your request (some of which are misguided) have triggered a pre-flight request for the cross origin request. This means that the browser will separately send an OPTIONS request to your server to ask it if it is OK to process this request before sending the actual request.
You have a couple of choices. First, remove the custom headers that are triggering the pre-flight request. Neither Content-Type or Access-Control-Allow-Origin are appropriate for a request you are sending from the browser. Content-Type belongs to the response and the client does not get to specify anything about what origins are allowed - that's for the server to decide.
If, for some reason, you can't stop the browser from triggering the pre-flight request, then you need to explicitly add support to your server for the OPTIONS pre-flight request. You can see how to use the cors module to do that here.

Resources