131

How to share cookies cross origin? More specifically, how to use the Set-Cookie header in combination with the header Access-Control-Allow-Origin?

Here's an explanation of my situation:

I am attempting to set a cookie for an API that is running on localhost:4000 in a web app that is hosted on localhost:3000.

It seems I'm receiving the right response headers in the browser, but unfortunately they have no effect. These are the response headers:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:3000
Vary: Origin, Accept-Encoding
Set-Cookie: token=0d522ba17e130d6d19eb9c25b7ac58387b798639f81ffe75bd449afbc3cc715d6b038e426adeac3316f0511dc7fae3f7; Max-Age=86400; Domain=localhost:4000; Path=/; Expires=Tue, 19 Sep 2017 21:11:36 GMT; HttpOnly
Content-Type: application/json; charset=utf-8
Content-Length: 180
ETag: W/"b4-VNrmF4xNeHGeLrGehNZTQNwAaUQ"
Date: Mon, 18 Sep 2017 21:11:36 GMT
Connection: keep-alive

Furthermore, I can see the cookie under Response Cookies when I inspect the traffic using the Network tab of Chrome's developer tools. Yet, I can't see a cookie being set in in the Application tab under Storage/Cookies. I don't see any CORS errors, so I assume I'm missing something else.

Any suggestions?

Update I:

I'm using the request module in a React-Redux app to issue a request to a /signin endpoint on the server. For the server I use express.

Express server:

res.cookie('token', 'xxx-xxx-xxx', { maxAge: 86400000, httpOnly: true, domain: 'localhost:3000' })

Request in browser:

request.post({ uri: '/signin', json: { userName: 'userOne', password: '123456'}}, (err, response, body) => {
    // doing stuff
})

Update II:

I am setting request and response headers now like crazy now, making sure that they are present in both the request and the response. Below is a screenshot. Notice the headers Access-Control-Allow-Credentials, Access-Control-Allow-Headers, Access-Control-Allow-Methods and Access-Control-Allow-Origin. Looking at the issue I found at Axios's github, I'm under the impression that all required headers are now set. Yet, there's still no luck...

enter image description here

Jenson M John
  • 4,972
  • 4
  • 25
  • 45
Pim Heijden
  • 3,603
  • 3
  • 8
  • 17
  • 4
    @PimHeijden take a look to this: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials maybe the use of withCredentials is what you need? – Kalamarico Sep 19 '17 at 13:02
  • 2
    Ok you are using request and i think this is not the best choice, take a look to this post and the answer, axios i think could be usefull to you. https://stackoverflow.com/questions/39794895/how-do-i-make-an-http-request-in-react-redux – Kalamarico Sep 19 '17 at 19:56
  • Thanks! I failed to notice that the `request` module is not meant for use in the browser. Axios seems to do a great job so far. I receive now both the header: `Access-Control-Allow-Credentials:true` and `Access-Control-Allow-Origin:http://localhost:3000` (used to enable CORS). This seems right but the `Set-Cookie` header doesnt do anything... – Pim Heijden Sep 23 '17 at 19:46
  • Same issue, but using directly Axios : https://stackoverflow.com/q/43002444/488666. While `{ withCredentials: true }` is indeed required by Axios side, server headers have to be checked carefully as well (see https://stackoverflow.com/a/48231372/488666) – Frosty Z Jan 15 '18 at 09:33
  • what server headers? – Pim Heijden Jan 16 '18 at 19:56

5 Answers5

202

What you need to do

To allow receiving & sending cookies by a CORS request successfully, do the following.

Back-end (server): Set the HTTP header Access-Control-Allow-Credentials value to true. Also, make sure the HTTP headers Access-Control-Allow-Origin and Access-Control-Allow-Headers are set and not with a wildcard *.

Recommended Cookie settings per Chrome and Firefox update in 2021: SameSite=None and Secure. See MDN documentation

For more info on setting CORS in express js read the docs here

Front-end (client): Set the XMLHttpRequest.withCredentials flag to true, this can be achieved in different ways depending on the request-response library used:

Or

Avoid having to use CORS in combination with cookies. You can achieve this with a proxy.

If you for whatever reason don't avoid it. The solution is above.

It turned out that Chrome won't set the cookie if the domain contains a port. Setting it for localhost (without port) is not a problem. Many thanks to Erwin for this tip!

Pim Heijden
  • 3,603
  • 3
  • 8
  • 17
  • 2
    I think you have this problem just because of the `localhost` check this here: https://stackoverflow.com/a/1188145 and also this may help your case (https://stackoverflow.com/questions/50966861/how-to-correct-set-the-cookies-for-a-subdomain-in-jsp/50988268#50988268) – Edwin Jun 22 '18 at 12:48
  • So you're refering to the remark that "the cookie domain must be omitted entirely"? – Pim Heijden Jun 24 '18 at 19:05
  • 5
    This answer helped me so much! Took a long time to find it. But I think the answer should mention that setting `Access-Control-Allow-Origin` to an explicit domain, not just `"*"` is also required. Then it would be the perfect answer – e.dan Jan 14 '19 at 12:18
  • Thanks. Must that header be specific though? I'd like to assume a wildcard also works. – Pim Heijden Jan 14 '19 at 12:24
  • 6
    this is good answer, and all setup for CORS, headers, backend and front end, and avoiding localhost with override /etc/hosts locally with a real subdomain, still I see postman shows a SET-COOKIE in response headers but chrome debug does not show this in response headers and also the cookie isn't actually set in chrome. Any other ideas to check? – bjm88 Apr 18 '19 at 05:27
  • 1
    @bjm88 Did you end up figuring this out? I'm in the exact same situation. The cookie is set properly when connecting from localhost:3010 to localhost:5001 but does not work from localhost:3010 to fakeremote:5001 (which points to 127.0.0.1 in my hosts file). It's the exact same when I host my server on a real server with a custom domain (connecting from localhost:3010 to mydomain.com). I've done all that's recommended in this answer and I tried lots of other things. – Form Aug 12 '19 at 21:45
  • I don't see how those headers could be different in Postman vs. Chrome. It's the hostname that counts for the cookie — localhost vs. fakeremote. – Pim Heijden Aug 13 '19 at 22:52
  • @Form have spent a long time searching this; your experience is the same as mine - that the same hostname (different port) will send cookies, but different hostname + port will not. This seems to run counter to the withCredentials spec. Did you make any progress?! – hagen Oct 16 '19 at 10:41
  • 1
    Yes, thank you, the answer is here. I don't fully understand your question, but I'll try to answer it quickly. Cookies don't respect ports. Just leave out the port from the cookie domain. Port is not part of the domain concept anyway. In case of truly different domains, use a proxy if you can. That way you completely avoid the CORS problem to begin with. – Pim Heijden Oct 16 '19 at 19:29
  • @hagen Unfortunately I don't remember... but I did make it work in the end. I should have gone back to this answer as soon as I found the solution. Anyway, I know I had issues with OPTIONS pre-flight requests at the same time, which required modifications on my server. You might want to make sure everything is OK on that front! – Form Oct 17 '19 at 20:41
  • 1
    In Angular, the required client side change is also to add withCredentials: true to the options passed to HttpClient.post, .get, options, etc. – Marvin Dec 05 '19 at 19:30
  • The CORS header Access-Control-Allow-Credentials and the JS-API props "withCredentials/credentials" define if cookies are received/sent by requests/responses and the read/write access to cookies is still handled by the browser's same-origin policy. See also the post here: https://stackoverflow.com/a/46464563 – olibur Mar 24 '20 at 13:55
  • In my case, I had to add the `Access-Control-Expose-Headers` header to give the client access to the `Set-Cookie` header. Here's a more in-depth description of what led me to that: https://stackoverflow.com/a/62821342/8479303 .. Maybe I've misunderstood something -- but if not, hopefully it helps someone :) – jnotelddim Jul 09 '20 at 18:36
  • @lennylip mentioned in his answer below, it is showing error for samesite and secure flag. Now secure flag will only work if we are operating over https. How to achieve this on local setup ? – nirmal patel Sep 09 '20 at 04:19
  • Not entirely sure but it is starting to sound a lot like there is no way to achieve this anymore and we all have to start using proxies. – Pim Heijden Sep 17 '20 at 15:07
  • @PimHeijden if we use Allow-origin to * then i am getting this error has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. – Aman Bansal Oct 08 '20 at 17:40
  • Yes, that's expected. The 'Access-Control-Allow-Origin' header should not be a wildcard for 'Access-Control-Allow-Credentials: true' (credentials mode 'include' ) to work. Replace the wildcard with a specific host – Pim Heijden Oct 14 '20 at 08:19
  • I had success using the following `Access-Control-Allow-Headers` with value set to `Accept,Accept-Language,Content-Language,Content-Type,Authorization,Cookie,X-Requested-With,Origin,Host` (Maybe some are in excess, but it worked this way) – Marcel Jan 21 '21 at 10:59
  • @Form we are facing the same issue. A cookie is not sent for other domains. Were you able to find the solution? – shwetap Jan 21 '21 at 20:32
  • @shwetap Actually as we were using the cookie for authentication purposes, we skirted the issue completely by using Basic Auth headers on all API requests instead (you can also use token-based auth with a "login" step if you want). In retrospect, that's a better design for an API anyway. We could only do that because we are in full control of the server itself, though. I suppose the "hope the browser sends the cross-origin cookie" approach is fraught with peril as cookies are under assault by tracking protection in modern browsers. – Form Jan 22 '21 at 21:13
25

Note for Chrome Browser released in 2020.

A future release of Chrome will only deliver cookies with cross-site requests if they are set with SameSite=None and Secure.

So if your backend server does not set SameSite=None, Chrome will use SameSite=Lax by default and will not use this cookie with { withCredentials: true } requests.

More info https://www.chromium.org/updates/same-site.

Firefox and Edge developers also want to release this feature in the future.

Spec found here: https://tools.ietf.org/html/draft-west-cookie-incrementalism-01#page-8

John Redd
  • 3
  • 1
LennyLip
  • 1,306
  • 15
  • 17
  • 9
    providing samesite=none and secure flag require HTTPS. How to achieve this in a local system where HTTPS is not an option? can we bypass somehow? – nirmal patel Sep 09 '20 at 04:22
  • @nirmalpatel Just remove the "Lax" value in Chome dev console. – LennyLip Oct 07 '20 at 05:10
  • @LennyLip I think that if you remove the "Lax" value, it actually still defaults to `Lax`, not `None`. (According to https://web.dev/samesite-cookies-explained/, and also MDN.) – Matt Browne May 06 '21 at 11:49
  • 1
    @MattBrowne it works for me. Anyway, in the last Chrome, we got EdithThisCookie tab where we can select "No restriction" values from the Dropdown menu for the SameSite. – LennyLip May 07 '21 at 08:37
10

In order for the client to be able to read cookies from cross-origin requests, you need to have:

  1. All responses from the server need to have the following in their header:

    Access-Control-Allow-Credentials: true

  2. The client needs to send all requests with withCredentials: true option

In my implementation with Angular 7 and Spring Boot, I achieved that with the following:


Server-side:

@CrossOrigin(origins = "http://my-cross-origin-url.com", allowCredentials = "true")
@Controller
@RequestMapping(path = "/something")
public class SomethingController {
  ...
}

The origins = "http://my-cross-origin-url.com" part will add Access-Control-Allow-Origin: http://my-cross-origin-url.com to every server's response header

The allowCredentials = "true" part will add Access-Control-Allow-Credentials: true to every server's response header, which is what we need in order for the client to read the cookies


Client-side:

import { HttpInterceptor, HttpXsrfTokenExtractor, HttpRequest, HttpHandler, HttpEvent } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable } from 'rxjs';

@Injectable()
export class CustomHttpInterceptor implements HttpInterceptor {

    constructor(private tokenExtractor: HttpXsrfTokenExtractor) {
    }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        // send request with credential options in order to be able to read cross-origin cookies
        req = req.clone({ withCredentials: true });

        // return XSRF-TOKEN in each request's header (anti-CSRF security)
        const headerName = 'X-XSRF-TOKEN';
        let token = this.tokenExtractor.getToken() as string;
        if (token !== null && !req.headers.has(headerName)) {
            req = req.clone({ headers: req.headers.set(headerName, token) });
        }
        return next.handle(req);
    }
}

With this class you actually inject additional stuff to all your request.

The first part req = req.clone({ withCredentials: true });, is what you need in order to send each request with withCredentials: true option. This practically means that an OPTION request will be send first, so that you get your cookies and the authorization token among them, before sending the actual POST/PUT/DELETE requests, which need this token attached to them (in the header), in order for the server to verify and execute the request.

The second part is the one that specifically handles an anti-CSRF token for all requests. Reads it from the cookie when needed and writes it in the header of every request.

The desired result is something like this:

response request

Twiggeh
  • 632
  • 1
  • 3
  • 18
Stefanos Kargas
  • 8,783
  • 21
  • 69
  • 90
  • what does this answer add to the existing one? – Pim Heijden Jun 25 '20 at 16:16
  • 3
    An actual implementation. The reason I decided to post it, is that I spend a lot of time searching for the same issue and adding pieces together from various posts to realize it. It should be much easier for someone to do the same, having this post as a comparison. – Stefanos Kargas Jun 29 '20 at 07:42
  • Showing setting `allowCredentials = "true"` in the `@CrossOrigin` annotation helped me. – ponder275 Jun 30 '20 at 20:37
  • @lennylip mentioned in his answer above, it is showing error for samesite and secure flag. How to achieve that with localhost server without a secure flag. – nirmal patel Sep 09 '20 at 04:17
  • 1
    just a tip here, angular default csrf interceptor implementation will not work, use this answer provided interceptor will work and it will attached cookie on GET calls too. – ExploreEv May 02 '21 at 19:51
4

For express, upgrade your express library to 4.17.1 which is the latest stable version. Then;

In CorsOption: Set origin to your localhost url or your frontend production url and credentials to true e.g

  const corsOptions = {
    origin: config.get("origin"),
    credentials: true,
  };

I set my origin dynamically using config npm module.

Then , in res.cookie:

For localhost: you do not need to set sameSite and secure option at all, you can set httpOnly to true for http cookie to prevent XSS attack and other useful options depending on your use case.

For production environment, you need to set sameSite to none for cross-origin request and secure to true. Remember sameSite works with express latest version only as at now and latest chrome version only set cookie over https, thus the need for secure option.

Here is how I made mine dynamic

 res
    .cookie("access_token", token, {
      httpOnly: true,
      sameSite: app.get("env") === "development" ? true : "none",
      secure: app.get("env") === "development" ? false : true,
    })
1

Pim's answer is very helpful. In my case, I have to use

Expires / Max-Age: "Session"

If it is a dateTime, even it is not expired, it still won't send the cookie to the backend:

Expires / Max-Age: "Thu, 21 May 2020 09:00:34 GMT"

Hope it is helpful for future people who may meet same issue.

Hongbo Miao
  • 31,551
  • 46
  • 124
  • 206