48

Attempting to implement an ajax login / signup process (no refresh site with authentication). Using cookies for preserving state. I thought I'd have this right by now but for some reason browser doesn't set cookies after it gets them back from the server. Can anyone help? Here are the request and response headers:

Request URL:http://api.site.dev/v1/login
Request Method:POST
Status Code:200 OK

Request Headers

Accept:application/json, text/plain, */*
Accept-Charset:ISO-8859-1,utf-8;q=0.7,*;q=0.3
Accept-Encoding:gzip,deflate,sdch
Accept-Language:en-US,en;q=0.8
Connection:keep-alive
Content-Length:57
Content-Type:application/json;charset=UTF-8
Host:api.site.dev
Origin:http://site.dev
Referer:http://site.dev/
User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11
withCredentials:true
X-Requested-With:XMLHttpRequest
Request Payload
{"email":"calvinfroedge@gmail.com","password":"foobar"}

Response Headers

Access-Control-Allow-Credentials:true
Access-Control-Allow-Headers:X-Requested-With, Content-Type, withCredentials
Access-Control-Allow-Methods:GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Origin:http://site.dev
Connection:Keep-Alive
Content-Length:19
Content-Type:application/json
Date:Tue, 08 Jan 2013 18:23:14 GMT
Keep-Alive:timeout=5, max=99
Server:Apache/2.2.22 (Unix) DAV/2 PHP/5.4.7 mod_ssl/2.2.22 OpenSSL/0.9.8r
Set-Cookie:site=%2B1THQQ%2BbZkEwTYFvXFVV5fxi00l2K%2B6fvt9SuHACTNsEwUGzDSUckt38ZeDsNbZSsqzHmPMWRLc84eDLZzh8%2Fw%3D%3D; expires=Thu, 10-Jan-2013 18:23:14 GMT; path=/; domain=.site.dev; httponly
X-Powered-By:PHP/5.4.7

I also see the cookie in chrome network tools, as returned from the server:

Response Cookies

Name: site
Value: %2B1THQQ%2BbZkEwTYFvXFVV5fxi00l2K%2B6fvt9SuHACTNsEwUGzDSUckt38ZeDsNbZSsqzHmPMWRLc84eDLZzh8%2Fw%3D%3D
Domain: .site.dev
Path: /
Expires: Session
Size: 196
Http: ✓
Calvin Froedge
  • 16,135
  • 16
  • 55
  • 61
  • Is this a domain-matching issue as per Section 4.3.2 of RFC2109? I'm not entirely clear on whether your host (api.site.dev) and cookie-domain (.site.dev) are "domain-matching" as required. The CORS side of this doesn't make this any easier to deduce, either :) – broofa Jan 08 '13 at 19:29
  • 1
    Possible duplicate of [CORS request - why are the cookies not sent?](http://stackoverflow.com/questions/8863571/cors-request-why-are-the-cookies-not-sent) – Michael Freidgeim May 11 '16 at 03:13

3 Answers3

36

Your AJAX request must be made with the "withCredentials" settings set to true (only available in XmlHttpRequest2 and fetch):

    var req = new XMLHttpRequest();
    req.open('GET', 'https://api.bobank.com/accounts', true); // force XMLHttpRequest2
    req.setRequestHeader('Content-Type', 'application/json; charset=utf-8');
    req.setRequestHeader('Accept', 'application/json');
    req.withCredentials = true; // pass along cookies
    req.onload = function()  {
        // store token and redirect
        let json;
        try {
            json = JSON.parse(req.responseText);
        } catch (error) {
            return reject(error);
        }
        resolve(json);
    };
    req.onerror = reject;

If you want a detailed explanation on CORS, API security, and cookies, the answer doesn't fit in a StackOverflow comment. Check out this article I wrote on the subject: http://www.redotheweb.com/2015/11/09/api-security.html

François Zaninotto
  • 7,068
  • 2
  • 35
  • 56
  • 7
    Problem:- A wildcard '*' cannot be used in the 'Access-Control-Allow-Origin' header when the credentials flag is true. Origin 'null' is therefore not allowed access. – ZiglioUK Jan 04 '16 at 22:04
  • Francois, I read your article, I am consuming one api which is a web service. It requires system authentication (Not a db driven). I use with credentials and able to login. Now is it possible to store this credentials in cookie and bypass authentication. Thanks for article. – Abel Jojo Jun 14 '17 at 19:05
  • 1
    You must also set the response header `Access-Control-Allow-Credentials` to `'true'`, otherwise the browser will block (deny) the request. – Tim Sep 23 '17 at 14:59
13

I had a similiar problem, and it turned out that the browser settings were blocking third-party cookies (Chrome > Settings > Advanced Settings > Privacy > Content Settings > Block third-party cookies and site data). Unblocking solved the problem!

Andrew M. Andrews III
  • 1,989
  • 18
  • 23
  • 4
    Indeed. And since that is the default setting in Safari, and possible in most other browsers, making a site, that relies on third-party-cookies via CORS should be done with some consideration. Few users may like to be told to enable third party cookies for a site to work. – Torin Finnemann Jun 03 '14 at 14:04
  • 2
    I just spent three hours troubleshooting and looking for support before I realized this was my problem. Thanks! – Chris Apr 10 '15 at 18:42
  • 16
    I am having the same problem, but "Block third-party cookies and site data" is already disabled. – Thayne May 20 '15 at 19:06
  • 1
    If you've chosen a solution that requires your users to alter a setting in Chrome then sadly it won't work for you in production – Peter Nixey Aug 12 '20 at 09:49
  • It also is default in 'Brave', good discussion. – emre-ozgun Mar 01 '22 at 01:34
2

I needed to pass cookies from multiple subdomains to a single API domain using AJAX and PHP and handeling CORS correctly.

This was the challenge and solution:

1 - Backend PHP on api.example.com.

2 - Multiple JS front ends such as one.example.com, two.example.com etc.

3 - Cookies needed to be passed both ways.

4 - AJAX call from multiple front-ends to PHP backend on api.example.com

5 - In PHP, I do not prefer to use $_SERVER["HTTP_ORIGIN"], not always reliable/safe in my opinion (I had some browsers where HTTP-ORIGIN was always empty).

The normal way to do this in PHP with single front end domain is starting PHP code with:

header('Access-Control-Allow-Origin: https://one.example.com');  
header('Access-Control-Allow-Headers: Origin, Content-Type, X-Auth-Token');  
header('Access-Control-Allow-Credentials: true');  

And in JS on one.example.com domain:

jQuery.ajax({
    url: myURL,
    type: "POST",
    xhrFields: {withCredentials: true},
    dataType: "text",
    contentType: "text/xml; charset=\"utf-8\"",
    cache: false,
    headers: "",
    data: myCallJSONStr,
    success: function(myResponse) {.....}

However, this is not workable as I am using multiple subdomains to call my API domain.

And this solution will NOT work as I want to pass on cookies:

header('Access-Control-Allow-Origin: *');  

It conflicts with the pass on cookie setting on the JS site:

xhrFields: {withCredentials: true}

Here is what I did:

1 - use GET parameter to pass the Subdomain.

2 - Hardcode the Main domain in PHP so only (all) Subdomains are allowed.

This is the JS/JQuery AJAX part of my solution:

function getSubDomain(){
    
    let mySubDomain = "";
    
    let myDomain = window.location.host;
    let myArrayParts = myDomain.split(".");
    if (myArrayParts.length == 3){
        mySubDomain = myArrayParts[0];
    }
    
    return mySubDomain;
    
}

And in the AJAX call:

let mySubDomain = getSubDomain();
if (mySubDomain != ""){
    myURL += "?source=" + mySubDomain + "&end"; //use & instead of ? if URL already has GET parameters
}

jQuery.ajax({
    url: myURL,
    type: "POST",
    xhrFields: {withCredentials: true},
    dataType: "text",
    contentType: "text/xml; charset=\"utf-8\"",
    cache: false,
    headers: "",
    data: myCallJSONStr,
    success: function(myResponse) {.....}

Finally, the PHP part:

<?php

$myDomain = "example.com";
$mySubdomain = "";

if (isset($_GET["source"])) {
    $mySubdomain = $_GET["source"].".";
}

$myDomainAllowOrigin = "https://".$mySubdomain.$myDomain;
$myAllowOrigin = "Access-Control-Allow-Origin: ".$myDomainAllowOrigin;

//echo $myAllowOrigin;

header($myAllowOrigin);  
header('Access-Control-Allow-Headers: Origin, Content-Type, X-Auth-Token');  
header('Access-Control-Allow-Credentials: true');

IMPORTANT, don't forget to set the cookies for all subdomains, in this case the domain for the cookie would be: .example.com (so with a dot in front of the main domain):

<?php

    //////////////// GLOBALS /////////////////////////////////
    
    $gCookieDomain = ".example.com";
    $gCookieValidForDays = 90;
    
    //////////////// COOKIE FUNTIONS /////////////////////////////////
    
    function setAPCookie($myCookieName, $myCookieValue, $myHttponly){
        global $gCookieDomain;
        global $gCookieValidForDays;
        
        $myExpires = time()+60*60*24*$gCookieValidForDays;
        setcookie($myCookieName, $myCookieValue, $myExpires, "/", $gCookieDomain, true, $myHttponly);   
        
        return $myExpires;
    }

This solution allows me to call the API on api.example.com from any subdomains on example.com.

NB. for situation where there is only a single calling subdomain, I prefer using .htaccess for setting CORS instead of PHP. Here is an example of .htaccess (linux/apache) for only one.example.com calling api.example.com:

<IfModule mod_headers.c>
    Header set Access-Control-Allow-Origin "https://one.example.com"
    Header set Access-Control-Allow-Headers "Origin, Content-Type, X-Auth-Token"
    Header set Access-Control-Allow-Credentials "true"
</IfModule>

And place this .htaccess in the root of api.example.com.

David
  • 3,285
  • 1
  • 37
  • 54
Al-Noor Ladhani
  • 2,413
  • 1
  • 22
  • 14