72

I am making an ajax request using $.ajax. The response has the Set-Cookie header set (I've verified this in the Chrome dev tools). However, the browser does not set the cookie after receiving the response! When I navigate to another page within my domain, the cookie is not sent. (Note: I'm not doing any cross-domain ajax requests; the request is in the same domain as the document.)

What am I missing?

EDIT: Here is the code for my ajax request:

$.post('/user/login', JSON.stringify(data));

Here is the request, as shown by the Chrome dev tools:

Request URL:https://192.168.1.154:3000/user/login
Request Method:POST
Status Code:200 OK

Request Headers:
Accept:*/*
Accept-Encoding:gzip,deflate,sdch
Accept-Language:en-US,en;q=0.8
Connection:keep-alive
Content-Length:35
Content-Type:application/x-www-form-urlencoded; charset=UTF-8
DNT:1
Host:192.168.1.154:3000
Origin:https://192.168.1.154:3000
Referer:https://192.168.1.154:3000/
User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.154 Safari/537.36
X-Requested-With:XMLHttpRequest

Form Data:
{"UserId":"blah","Password":"blah"}:

Response:

Response Headers:
Content-Length:15
Content-Type:application/json; charset=UTF-8
Date:Sun, 16 Mar 2014 03:25:24 GMT
Set-Cookie:SessionId=MTM5NDk0MDMyNHxEdi1CQkFFQ180SUFBUkFCRUFBQVRfLUNBQUVHYzNSeWFXNW5EQXNBQ1ZObGMzTnBiMjVKWkFaemRISnBibWNNTGdBc1ZFcDNlU3RKVFdKSGIzQlNXRkkwVjJGNFJ6TlRVSHA0U0ZJd01XRktjMDF1Y1c1b2FGWXJORzV4V1QwPXwWf1tz-2Fy_Y4I6fypCzkMJyYxhgM3LjVHGAlKyrilRg==; HttpOnly
Matt Fichman
  • 5,458
  • 4
  • 39
  • 59
  • 3
    So this might be an old thread, but I stumbled upon it looking for something else and I noticed that your request had `DNT: 1` in the header. If I recall, this is Do Not Track and the browsers is requesting to not allow cookies to be set. – thecodegoddess Feb 22 '17 at 01:50
  • If you're having this issue with Apollo, check out [this section](https://www.apollographql.com/docs/react/recipes/authentication.html) of their documentation – Peter Berg Nov 27 '17 at 20:55

5 Answers5

79

OK, so I finally figured out the problem. It turns out that setting the Path option is important when sending cookies in an AJAX request. If you set Path=/, e.g.:

Set-Cookie:SessionId=foo; Path=/; HttpOnly

...then the browser will set the cookie when you navigate to a different page. Without setting Path, the browser uses the "default" path. Apparently, the default path for a cookie set by an AJAX request is different from the default path used when you navigate to a page directly. I'm using Go/Martini, so on the server-side I do this:

session.Options(session.Options{HttpOnly: true, Path:"/"})

I'd guess that Python/Ruby/etc. have a similar mechanism for setting Path.

See also: cookies problem in PHP and AJAX

Community
  • 1
  • 1
Matt Fichman
  • 5,458
  • 4
  • 39
  • 59
64

@atomkirk's answer didn't apply to me because

  1. I don't use the fetch API
  2. I was making cross-site requests (i.e. CORS)

NOTE: If your server is using Access-Control-Allow-Origin:* (aka "all origins"/"wildcard origins"), you may not be able to send credentials (see below).

Setting withCredentials/credentials in browser/client request

As for the fetch API; CORS requests will need {credentials:'include'} for both sending & receiving cookies

For CORS requests, use the "include" value to allow sending credentials to other domains:

fetch('https://example.com:1234/users', {   
            credentials: 'include' 
})

... To opt into accepting cookies from the server, you must use the credentials option.


{credentials:'include'} is equivalent to xhr.withCredentials=true

In fact, checkfetch polyfill code (a Javascript implementation)

if (request.credentials === 'include') {
      xhr.withCredentials = true
 } 

As @haneSmitter says in the comments, if fetch is implemented natively by your browser, you don't need a Javascript polyfill, so use the appropriate value for fetch credentials property. fetch allows three values for { credentials:'...' } configuration ( "omit", "same-origin", or "include"), whereas xhr is only a boolean (two values: true or false). Read more at the fetch spec here


If you're using jQuery, you can set withCredentials (remember to use crossDomain: true) using $.ajaxSetup(...)

$.ajaxSetup({
             crossDomain: true,
             xhrFields: {
                 withCredentials: true
             }
         });

If you're using AngularJS, the $http service config arg accepts a withCredentials property:

$http({
    withCredentials: true
});

If you're using Angular (Angular IO), the common.http.HttpRequest service options arg accepts a withCredentials property:

this.http.post<Hero>(this.heroesUrl, hero, {
    withCredentials: true
});

As for the request, when xhr.withCredentials=true; the Cookie header is sent

Before I changed xhr.withCredentials=true

  1. I could see Set-Cookie name & value in the response, but Chrome's "Application" tab in the Developer Tools showed me the name and an empty value
  2. Subsequent requests did not send a Cookie request header.

After the change xhr.withCredentials=true

  1. I could see the cookie's name and the cookie's value in the Chrome's "Application" tab (a value consistent with the Set-Cookie header).
  2. Subsequent requests did send a Cookie request header with the same value, so my server treated me as "authenticated"

Setting Access-Control... headers in server response

As for the response, if your request is a cross-site request, the server will need certain Access-Control... headers

For example, I configured my server to return these headers:

  • Access-Control-Allow-Credentials:true
  • Access-Control-Allow-Origin:https://{your-origin}:{your-port}

Until I made this server-side change to the response headers, Chrome logged errors in the console like

Failed to load https://{saml-domain}/saml-authn: Redirect from https://{saml-domain}/saml-redirect has been blocked by CORS policy:

The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'. Origin https://{your-domain} is therefore not allowed access.

The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

After making this Access-* header change, Chrome did not log errors; the browser let me check the authenticated responses for all subsequent requests.

Can't use wildcard/asterisk Access-Control-Allow-Origin:*, use dynamic value based on request

Using withCredentials and setting Access-Control-Allow-Origin:* (aka wildcard, allowing "all origins") won't work. As seen in the comments and described here (thanks to @ChandanBhattad). You might see this error:

The CORS request was attempted with the credentials flag set, but the server is configured using the wildcard ("*") as the value of Access-Control-Allow-Origin, which doesn't allow the use of credentials.

Therefore if you control the server and you want to allow cross-origin requests from any origin, you must dynamically decide the response value based on the request origin, or as MDN says:

If the server supports clients from multiple origins, it must return the origin for the specific client making the request.

For example, your server should inspect the request, if the request comes from http://www.example.com, your response headers should include:

Access-Control-Allow-Origin: http://www.example.com
Vary: Origin

Note the use of the Vary response header, which MDN says:

indicates to browsers that server responses can differ based on the value of the Origin request header.

How to change your server response headers depends on your server technology.

Nate Anderson
  • 18,334
  • 18
  • 100
  • 135
  • Really helped me a lot. Works :D – Manish Pradhan Feb 12 '18 at 02:44
  • Thanks for the comprehensive answer. Worked for me. – squishyMage May 31 '18 at 07:46
  • 4
    `for both sending & RECEIVING cookies` That did help. – volkovs Jun 10 '19 at 07:18
  • 1
    This seems to be the right answer. Processed user authentication in ajax POST request, but cookie wasn't set. Interestingly this problem occured only on mobile browsers, on desktop not. xhrFields: { withCredentials: true } parameter apparently fixed the issue. Too bad I found this answer after fixing the problem, it was driving me crazy for a few days. – beatcoder Nov 12 '20 at 06:36
  • 1
    Sendering and RECEIVING.. for real. I was POSTing to my login endpoint with { withCredentials: false } set and spent 4+ hours trying to figure out why the cookies we not being set. – eth0 Mar 24 '21 at 02:15
  • 1
    Thank you. I was in the same situation as you, using cross-site cookies (my backend is running in a host and the frontend in another host). I had everything in place, except that I wasn't using the withCredentials flag in the login request, only in subsequent requests. Once I used it in the login request (had to configure my CORS policies in the right way, as it doesn't like when allowed origins is *), it worked fine. – Oscar Calderon May 18 '21 at 15:35
  • 1
    Thanks. This doesn't seem to work if I have set "access-control-allow-origin:*" . Basically - "credentials": "include" is not allowed when access-control-allow-origin is set to * ? – Chandan Bhattad May 26 '21 at 12:47
  • 1
    Good point from @ChandanBhattad - this approach won't work if you allow *all* origins, as [described here](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSNotSupportingCredentials): "The CORS request was attempted with the credentials flag set, but the server is configured using the wildcard ("*") as the value of Access-Control-Allow-Origin, which doesn't allow the use of credentials." – Nate Anderson May 27 '21 at 17:57
  • 1
    yeah. I had a use case where i wanted to set it for any origin. as a workaround - I am now setting access-control-allow-origin header value to the request origin :) – Chandan Bhattad May 28 '21 at 14:38
  • That is a nice solution, I used the same "dynamic" approach; it requires the server to inspect the request, and repeat the *request's origin*, as the Access-Control-Allow-origin value in the response, right? So if request came from "a", it would allow origin "a", etc... Some folks may not have a server "smart enough" to do that. – Nate Anderson May 28 '21 at 18:06
  • 1
    `{credentials:'include'}` just sets `xhr.withCredentials=true`. I find this confusing since **fetch** API in browsers was not built on top of **xhr**. – hane Smitter Dec 20 '22 at 12:43
  • @haneSmitter I think `fetch(...,{credentials:'include'})` is *almost equivalent* to `xhr.withCredentials=true`, but I don't think it's correct to say the former "just sets" the latter... `fetch` is a different specification than `xhr`, a different implementation. [`fetch` allows **three values** for `{ credentials:'...' }`](https://fetch.spec.whatwg.org/#concept-request-credentials-mode) configuration ( "omit", "same-origin", or "include"), whereas `xhr` is only a boolean (**two values**: true or false). Maybe this is confusing, but the specs clarify the difference in behavior. – Nate Anderson Dec 21 '22 at 15:14
  • @haneSmitter Now I see what you mean! I myself wrote "{credentials:'include'} just sets xhr.withCredentials=true". You're right, that's confusing, fetch is a browser API, I was referring to the fetch *polyfill* (Javascript-based replacement for browsers that did not support fetch). I'll clarify my answer, thanks! – Nate Anderson Jun 16 '23 at 15:33
45

If you're using the new fetch API, you can try including credentials:

fetch('/users', {
  credentials: 'same-origin'
})

That's what fixed it for me.

In particular, using the polyfill: https://github.com/github/fetch#sending-cookies

atomkirk
  • 3,701
  • 27
  • 30
  • 1
    Im with @jag on this one!! I just spent 4 hours trying to login via ajax using passport.js... total mystery until I hit on the network response returning the cookie. It simply wouldn't save. Your solution fixed it. Cheers – Chris GW Green Oct 28 '16 at 22:13
  • After adding credentials and path in the set-cookie response, it works for me – Kumaresan Lc Apr 30 '17 at 07:16
  • 1
    Note: if your api lives on another domain, you'll need to use `credentials: 'include'`. Also, if you're having this issue with apollo, check out [this section](https://www.apollographql.com/docs/react/recipes/authentication.html) of their docs. – Peter Berg Nov 27 '17 at 20:59
2

This may help somebody randomly falling across this question.

I found forcing a URL with https:// rather than http:// even though the server hasn't got a certificate and Chrome complains will fix this issue.

CResults
  • 5,100
  • 1
  • 22
  • 28
2

In my case, the cookie size exceeded 4096 bytes (Google Chrome). I had a dynamic cookie payload that would increase in size.

Browsers will ignore the set-cookie response header if the cookie exceeds the browsers limit, and it will not set the cookie.

See here for cookie size limits per browser.

I know this isn't the solution, but this was my issue, and I hope it helps someone :)

jenkizenki
  • 741
  • 6
  • 14