66

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,188
  • 4
  • 35
  • 57
  • 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

75

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,188
  • 4
  • 35
  • 57
54

@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-Origins:* (aka "all origins"/"wildcard origins"), you may not be able to send credentials (see below).

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'} just sets xhr.withCredentials=true

Check fetch code

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

So plain Javascript/XHR.withCredentials is the important part.


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"

As for the response: the server may 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}

EDIT: this approach won't work if you allow all origins/wildcard origins, as described here (thanks to @ChandanBhattad) :

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.

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.

The Red Pea
  • 14,933
  • 15
  • 89
  • 118
  • 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
  • 3
    `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." – The Red Pea 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. – The Red Pea May 28 '21 at 18:06
41

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,379
  • 26
  • 28
  • 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
1

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,071
  • 1
  • 21
  • 28
1

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
  • 646
  • 6
  • 14