You can not set the 'Cookie' header when making a XMLHttpRequest. Attempting to do so results in a 'Refused to set unsafe header "Cookie"' error in Chrome.

The W3C spec lists Cookie as one of the headers that a XMLHttpRequest is not allowed to set manually, See http://www.w3.org/TR/XMLHttpRequest/#the-setrequestheader-method.

This, coupled with the fact that the Access-Control-Allow-Origin header is not set, seems to make it impossible for remote applications to authenticate against the RESTServer.

Here is a a code example of an attempt at making an authenticated AJAX request.

$.support.cors = true;

var endpoint = 'http://www.example.com/api';
  $.ajax({
    type: "GET",
    dataType: 'json',
    headers: {"Accept": "application/json"},
    url: endpoint + '/user/1.json',
    crossDomain: true,
//    headers: {'Cookie' : 'SESS83c26757d9118c6bafca4e38b2233563=G9oaLEcjP1BPlZQ5E4eSSgu0mfk2WqWrggxLNo5DsLk'},
    beforeSend: function(xhr) {
        xhr.setRequestHeader("Cookie", 'SESS83c26757d9118c6bafca4e38b2233563=G9oaLEcjP1BPlZQ5E4eSSgu0mfk2WqWrggxLNo5DsLk');  
    },
    success: function(data, textStatus, jqXHR){
      console.log('OK!');
    },
    error: function(jqXHR, textStatus, errorThrown){
      console.log(errorThrown);
    }
  });

Results in:

Refused to set unsafe header "Cookie"
1.jsonGET http://www.example.com/api/user/1.json 401 (Unauthorized: Access denied for user anonymous)

Can you you a header other than Cookie, or any of the other restricted headers for the authentication?

CommentFileSizeAuthor
#14 QQ20170521-0.png37.68 KBhubobbb
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

voxpelli’s picture

Status: Active » Closed (works as designed)

This does not make it impossible for remote web applications to authenticate with the REST Server. This only means that you can't have a different Cookie-session when doing AJAX than when doing regular requests in the browser.

REST Server has nothing to do with authorization - it's the job for authentication modules and you are free to create new modules which extends the functionality of Services. That is very much possible. The out of the box Services module provides the possibility to use Drupals own sessions, which has the limitations you mention, and to use OAuth (which is currently pending a D7 port of OAuth).

To get cross-domain working - then take a look at this module: https://github.com/simme/drupal-services_accept_origin

mrfelton’s picture

Thanks for your response, and for the list to the module. I'll port this over to Drupal 7.

...But I don't have any cookies in my browser already for the domain in question, so why would I not be able to use the session ID that I got back from an ajax login call as the Cookie in my subsequent ajax request?

voxpelli’s picture

You would need to set the cookie for real and I believe only the domain owning the cookie is allowed to do so. You're not only gaining access to your app. You're actually logging someone in to the site. So there's probably some browser security in place to stop that.

mrfelton’s picture

I ported that mod to D7:
https://github.com/systemseed/services_accept_origin

Seems to work ok.

These requests work ok from my android, but not through the Ripple android emulator, which runs as a Chrome extension. Maybe it has something todo with the fact that chromes own cookies are interfering somehow.

lucas.constantino’s picture

Hi there

I found this issue while trying to make a Drupal REST interface for AngularJS applications. After some digging I found a way to do that. The idea consists in:

1 - Making a normal login authentication provided by the Services module;
2 - Holding the "session_name" and "sessid" provided as response from the authentication;
3 - Making all subsequent request with a custom named header called "Authentication", with the value "[session_name]=[sessid]";
4 - Hooking Drupal's init to read this custom header and setting the session name and cookie accordangly;
5 - Running the "_drupal_session_read($sid)" (althought Drupal tells me not to) with the "sessid" as argument.

This is a TREMENDOUS hack; or at least it seams to me it is.

Here is the code, which could obviously be wrapper not to affect all request, but the ones directed to a Service endpoint:

<?php
function custom_services_init() {
    // Accept custom request header;
    drupal_add_http_header('Access-Control-Allow-Headers', 'Authentication');

    if (isset($_SERVER['HTTP_AUTHENTICATION'])) {
      list($session_name, $sessid) = explode('=', $_SERVER['HTTP_AUTHENTICATION']);
      session_name($session_name);
      $_COOKIE[session_name()] = $sessid;
      _drupal_session_read($sessid);
    }
}
?>

I hope someone else points me in a better direction.

jbeuckm’s picture

Is there an update to this? I am also looking to build an authenticated AngularJS app with Services module.

jbeuckm’s picture

Ok I have auth working from an Angular client in the browser without a hack. Let the browser build the cookie header by setting

document.cookie=sessionname+"="+sess_id.

I also installed the CORS module and configured a domain with

api/*|<mirror>|*|Content-Type,X-CSRF-Token|true

...where "api" is my Services endpoint.

Here is the project underway: https://github.com/jbeuckm/drupal-spa

itamair’s picture

Hi jbeuckm, we are facing similar matters in our angular application that seems not to digest/set the Cookie header when connecting do Drupal Services (actually just with Safari or Internet Explorer. Chrome and Firefox seem don't complain about all this).

I had a quick look to your application code drupal-spa (thanks!), but didn't find anywhere you set

document.cookie= ..

Where should it be? If still there ...
Is this still your adviced & feasible solution, or something changed in your code in the meanwhile?
Did you find better or alternative preferred ways?

I was also liking #6 (lucas.constantino's ...) solution as well, as my second possibility to try.

Looking for a possibile further feedback on this last #6 and #7 proposed solutions.

tengoku’s picture

Hi.

I'm using CORS Module and the patch for #5, except the part

drupal_add_http_header('Access-Control-Allow-Headers', 'Authentication');

.

This add header nullifies what i put on CORS:

api/*||*|Content-Type,X-CSRF-Token,Authentication, Origin, Cookie|true

Works well. I think if we putt Cookie on CORS Works also...

williamsowen’s picture

I'm also stuck with this issue - I've implemented what tengoku mentioned in #9 - but now I constantly get ["CSRF validation failed"].. I'm passing the X-CSRF-Token, not sure why it doesn't work

f0ns’s picture

I had the same issue as willamsowen,

I had to hack Services.module to fix this. I couldn't log out or create nodes so I changed

On line 581
if ($controller['callback'] != '_user_resource_get_token') {

too

if ($controller['callback'] != '_user_resource_get_token' && $controller['callback'] != '_user_resource_logout' && $controller['callback'] != '_node_resource_create') {

Not sure if this is the way to go but I don't get it to work in another way.

Otherwise the token always returns invalid because it checks with the original session and with the code of #5 we create a new session.

williamsowen’s picture

I had to hack the services.module to get this working just like Fons Vandamme. Pretty dodgy, but for test purposes I had to comment out line 589: // return t('CSRF validation failed'); - for something to work! ..Until a fix pops up.

I still have to use the 'Cookie fix' as suggested in #5 - but every time I hit an endpoint, a new session for user 0 gets created in the sessions table. It gets pretty clogged up, - not sure what to do? Has anyone experienced this?

omarlopesino’s picture

#5 solution worked for me! Thanks!

I had to add something to the code to completely authenticate the user, maybe risky. I had to add the following line at the end of the code.

drupal_session_started(TRUE);

If you don't make this Drupal don't consider user is logged in, even is loaded on $GLOBALS['user'].

hubobbb’s picture

FileSize
37.68 KB

#5 #9 works ok ,my test method here .

target file ../modules/services/servers/rest_server/includes/RESTServer.inc

find
// Set the content type and render output.
drupal_add_http_header('Content-type', $mime_type);

you can add new one ,like :
//my start
drupal_add_http_header('Access-Control-Allow-Origin', "*");
drupal_add_http_header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
drupal_add_http_header('Access-Control-congyuexpg', "congyuexpg");
drupal_add_http_header('Access-Control-Allow-Headers', 'Authentication');
//my end
drupal_add_http_header('Access-Control-Allow-Origin', "*"); is not safe ,just test here.

result .
r

gillarf’s picture

I am having exactly this problem with an Angular Cordova app. I am getting rather desperate with this now.

I have tried #5 but it does not seem to work for me.

Here is my CORS line:
*|http://localhost:8100|GET, PUT, POST, OPTIONS|Authentication, Authorization, Origin, Content-Type, X-CSRF-Token|true

I am successfully logging in and getting a X-CSRF-Token

Here is the angular code:

$http({
method : 'POST',
url : Constants.API.baseUrl+'/api/system/connect',
dataType : 'json',
headers: {
'X-CSRF-Token': xsrfToken,
'Authorization': [session_name]=[sessid]
},
withCredentials : 'true',
})

I get 401 (Unauthorized : CSRF validation failed.

Can anyone point me in the right direction please.

anrikun’s picture

The best workaround I have found so far is to use a custom X-Cookie header.

In your xhr use:

headers: {
  'X-Cookie': '[session_name]=[sessid]'
},

Server side, add this at the bottom of your settings.php file:

<?php
if (arg(0) == 'your_endpoint_name') {
  if (isset($_SERVER['HTTP_X_COOKIE']) && preg_match('`^S?SESS[0-9a-f]{32}=[\w-]+$`', $_SERVER['HTTP_X_COOKIE'])) {
    list($name, $value) = explode('=', $_SERVER['HTTP_X_COOKIE']);
    if (!array_key_exists($name, $_COOKIE)) {
      $_COOKIE[$name] = $value;
    }
  }

  drupal_add_http_header('Access-Control-Allow-Origin', '*');
  drupal_add_http_header('Access-Control-Allow-Headers', 'Content-Type, X-CSRF-Token, X-Cookie');
  drupal_add_http_header('Access-Control-Max-Age', 600);
}
?>
klakla.develop’s picture

I experienced the same problem. And use anrikun's solution for me to develop an ionic version 4 application which can not send cookies through the header http but can get anrikun method that changes from a cookie to an x-cookie. It works for me and it works well with each other.

ionic / angular code sent post method to drupal 7 service api/file

import { HttpClient, HttpHeaders } from '@angular/common/http';
.....

constructor(private http: HttpClient .....
let datas = {
      filename : filesname,
      target_uri : "pictures/"+filesname,
      filemime: "image/jpg",
      file : '/9j/4AAQSkZJRgABAQAAAQABAAD/...'
}
const httpOptions = {
      headers: new HttpHeaders({
        'Content-Type': 'application/json',
        'X-CSRF-TOKEN': tokens,
        'x-Cookie' : Cookies
      }),
    };
    return this.http.post(`https://localhost/api/api/file`,datas, httpOptions).pipe(
        tap(async (res: Authresponse) => {
          console.log(res);
          if (res) {
            console.log('seccess');
          }
        }, (error) => {
          console.log('error : ', error.error[0]);
        })
      );

Server side, add this at the Begin of your settings.php file:

  if (isset($_SERVER['HTTP_X_COOKIE']) && preg_match('`^S?SESS[0-9a-f]{32}=[\w-$
    list($name, $value) = explode('=', $_SERVER['HTTP_X_COOKIE']);
    if (!array_key_exists($name, $_COOKIE)) {
      $_COOKIE[$name] = $value;
    }
  }

  drupal_add_http_header('Access-Control-Allow-Origin', '*');
  drupal_add_http_header('Access-Control-Allow-Headers', 'Content-Type, X-CSRF-$
  drupal_add_http_header('Access-Control-Max-Age', 600);