Authentication and authorization with Services 3.5 CSRF token

monaw's picture

I have a mobile application that use to work with Services 3.3 using REST and JSON. I upgraded to Services 3.5 and the authentication/authorization stopped working.

I've been struggling with this for a few weeks and would appreciate any help.

Here are the step our application use to perform which worked in Services 3.3:

  1. Post to http://myserver.com/mobile/system/connect.json with username and password
  2. Save return values (session_name and sessid) from previous call
  3. Post to http://myserver.com/mobile/system/get_variable.json with header "Cookie" set to session_name=sessid from last step

It is my understanding that Services 3.5 now requires an extra step after login to retrieve the a CSRF token. This is what the app is doing now:

  1. Post to http://myserver.com/mobile/system/connect.json with username and password
  2. Save return values (session_name and sessid) from previous call
  3. Post to http://myserver.com/mobile/user/token with header "Cookie" set to session_name=sessid
  4. Save return token value
  5. Post to http://myserver.com/mobile/system/get_variable.json with 2 header variables: "Cookie" set to session_name=sessid and "X-CSRF-Token" set to token

But this is not working; I keep getting the 401 Access denied for user anonymous.

I read that the X-CSRF-Token is needed for POST, PUT and DELETE, but not GET.

I tried a GET service call which worked before also (http://myserver.com/mobile/presentations) but that also failed with the same 401 Access denied for user anonymous.

Please help.

Comments

Solve problem related with X-CSRF

-enzo-'s picture

Hello monaw

I have the same issue for and application in backbone.js against services, I resolve this getting the token and force to set a request header for following services call as you can see in the following code

$.ajax({
            url:"/services/session/token",
            type:"get",
            dataType:"text",
            error:function (jqXHR, textStatus, errorThrown) {
              alert(errorThrown);
            },
            success: function (token) {
                $.ajaxSetup({
                  beforeSend: function (request) {
                    request.setRequestHeader("X-CSRF-Token", token);
                  },
                });
            }
          });

You can review the full example at github: https://github.com/enzolutions/drupal-backbone-user-manager/blob/master/...

Greetings from Costa Rica.

enzo - Eduardo Garcia

Hi enzo, thank you for your

monaw's picture

Hi enzo, thank you for your reply.

I tried that in my app and I did get back the same token value I got when my app called http://myserver.com/mobile/user/token so I think that's the same.

What does your client application do after it gets the token?

This is where my app is failing, I am getting either 401 Access denied for user anonymous or "CSRF validation failed" when I tried to do the next step (retrieving a variable):

HTTP_X_CSRF_TOKEN or X-CSRF-Token

monaw's picture

Is it proper header name for the token X-CSRF-Token or HTTP_X_CSRF_TOKEN?

Hi monaw For me the header

-enzo-'s picture

Hi monaw

For me the header variable X-CSRF-Token works.

Is you check the code what I did in jquery is force all following request to service set the header, that is necessary for PUT, POST and DELETE ( UPDATE, ADD and DELETE)

In my case without that token I got the error 401 you mentioned above.

Regards.

enzo - Eduardo Garcia

Headers

monaw's picture

Hi Enzo, I did look at your code but I don't use jQuery so I have a question, after you retrieve the token, in all your subsequent calls, do you have both the Cookie and X-CSRF-Token headers set every time?

Correct

-enzo-'s picture

Correct

enzo - Eduardo Garcia

I've reverted back to Services 3.3

monaw's picture

Thanks Enzo for all your help but since I still cannot get authorization to work with the new Services 3.5, I've reverted back to 3.3 which worked fine for my app. I have several open Services issues in their queue and am still hoping the module maintainer will respond...

Hi Enzo: I did the security

monaw's picture

Hi Enzo: I did the security upgrade to Services 7.x-3.7 and am back to the 401 CSRF validation failed problem again in my mobile app.

I looked at your code but couldn't find where you are actually logging in for the user...

My question is what are the steps that your code uses which worked? Is it:

  1. Post to http://myserver.com/myendpoint/user/login.json with header parameters username and password
  2. Retrieve sessionid and session_name from response
  3. Post to http://myserver.com/myendpoint/user/token with session_name=sessionid in the header
  4. Retrieve CSRF token from response
  5. All subsequent requests to services include session_name=sessionid and X-CSRF-Token in the header

or does #3 come first (without session_name=sessionid in the header)? I'm confused about the order...

Hi monaw So besides to

-enzo-'s picture

Hi monaw

So besides to downgrade the services version, you did the header or you resolve in other way?

enzo - Eduardo Garcia

Downgrading service was

monaw's picture

Downgrading service was enough to get my mobile app working again (: The only header we need is the Cookie header which we already had.

Similar problem

kerry.t.johnson's picture

I know this is late to the topic, but maybe this will help someone else.

I too have a mobile app that was working in a previous version of Services/REST and I too had a similar problem to monaw. Enzo's example helped.

Don't ask me -why- but I had to remove my code that used the session Cookie and only use the token. Once I did that, it started working again (that is to say, I moved on to the next problem).

Thanks for your input! I

monaw's picture

Thanks for your input! I will give that a try when we move forward to upgrade our Services again.

That's very odd...are you

monaw's picture

That's very odd...are you allowing anonymous user to use that service?

Ok, I finally solved my

monaw's picture

Ok, I finally solved my authentication problem!

It turns out the token is returned from the login services call, along with the session ID and session name and a bunch of other info. So I just get it and use it the same way I use the session ID and session name (I add them to all future request's header).

Hmm, I don't see how this will make the server safer...no additional calls...just another piece of information...oh well.

Sigh...wish this was better documented. Hope this will help someone else.

Post code

mastermindg's picture

monaw....can you post the code you are using to login? I'm having similar issues and it would be nice to see how the call is organized.

Thanks!

Sigh...when I went back to my

monaw's picture

Sigh...when I went back to my mobile code, it doesn't work again...errrr, this is really frustrating!

monaw's picture

Ok after much struggling and debugging, I have it working again.

It turns out that part of the problem is that my mobile app logged in successfully once and all attempts to login after that failed with a 401 "CSRF validation failed" error. This suggests that a single user account cannot be used to simultaneously login from several devices...this is gonna be a problem for us.

But anyway, if this will help others...if you are getting the 401 CSRF error when your app tries to login, the app should retrieve the user's token from your server's user/token endpoint (I've seen other places where people said to use session/token but I don't have that, I have user/token; I'm using Services 3.7...anway) and then set the token you got into another request's header in the X-CSRF-Token variable. Then you can make the user/logout.json request and that should successfully log the user out of Drupal. NOW, you can do login again and it should work.

I cannot post our entire code because it is proprietary but I will post excerpts...our app is written in javascript using Titanium Studio:

var loginReq = Ti.Network.createHTTPClient({
onload: function(event)
    {
            // you've successfully logged in, do whatever you want now
  },
onerror: function(event)
   {
      Ti.API.error(this.responseText);   
       Ti.App.token = null;
       switch (this.status)
       {
          case 401:
              if ( this.responseText == '["Wrong username or password."]' )
              {
                  //-- Invalid Username/Password
                 Ti.App.fireEvent('logEvent', {
                       logType: Ti.App.LOGIN,
                     username: username.value,
                      logResult: '0'
                   });
                    alert('Invalid Username/Password');
                  break;
             }
             
               else if ( this.responseText == '["CSRF validation failed"]' )
              {
                  Ti.API.warn('User is already logged in, attempting logout');
                 var getToken = Ti.Network.createHTTPClient({
                       onload:function(event)
                     {
                          Ti.API.info('Successfully retrieved token!');
                            var response = JSON.parse(this.responseText);
                          Ti.App.token = response.token;
                         Ti.API.info ( "token : " + Ti.App.token );
                          
                           // Now try logout
                          var logout = Ti.Network.createHTTPClient({
                             onload:function(event)
                             {
                                  tryLogin(username.value,password.value, Ti.App.token );
                                    Ti.API.info('Successfully logged out of drupal');
                                },
                             onerror: function(event)
                               {
                                  Ti.API.info('Error logging out of drupal');
                              },
                             timeout:Ti.App.NETWORK_TIMEOUT
                         });
                            logout.open('POST',Ti.App.LOGOUT_REQUEST);
                           logout.setRequestHeader("X-CSRF-Token",Ti.App.token);
                            logout.send();
                     },
                     onerror: function(event)
                       {
                          Ti.API.info('Error getting token');
                      },
                     timeout:Ti.App.NETWORK_TIMEOUT
                 });
                    getToken.open('POST',Ti.App.TOKEN_REQUEST );
                 getToken.send();                   
                   break;
             }
              else
               {
                  break;
             }
          // handle other case
       }
  }
})

Hope this will help someone.

another titanium example for drupal

taggartj's picture

yes this took me a bit to work out ...here is a simple log in / create node example:

var mydrupalrest ="http://192.168.1.8/ilocal/theend/";
Ti.App.Properties.setString("mydrupalrest", mydrupalrest);


var win = Titanium.UI.createWindow({
    title:'TEST DRUPAL SERVICES',
    backgroundColor:'#fff'
});


//view for login
 
var loginview = Ti.UI.createView({
    backgroundColor:'blue',
    borderRadius:5,
    layout:'vertical',
    top:120,
    height:300,
    width:300
});
 
var username = Ti.UI.createTextField({
    hintText:'Enter your username',
    top:10,
    left:10,
    right:10
});
loginview.add(username);
  
var password = Ti.UI.createTextField({
    hintText:'Enter your password',
    passwordMask:true,
    top:5,
    height: 45,
    left:10,
    right:10
});
loginview.add(password);
  
var dologbutton = Ti.UI.createButton({
    title:'Login',
    left:50,
    right:50,
    top:8,
    height: 35
});
loginview.add(dologbutton);





//button click function
dologbutton.addEventListener('click',function(e)
{
   Titanium.API.info("You clicked the button to do login");
    
    username.blur();
    password.blur();
     
       
    var login_URL = mydrupalrest +"user/login.json";
     var theuser = {
       "username":username.value,
       "password":password.value
   };
     
     
    var xhr=Titanium.Network.createHTTPClient();
        xhr.setRequestHeader("Content-Type","application/json");
        xhr.open("POST", login_URL);
      xhr.send(JSON.stringify(theuser));
   
xhr.onload = function(){
     //alert("responseText: " + this.responseText);
      
     if(this.status == '200'){
       alert('json data  =' + this.responseText);
         

//phrase it
var json = JSON.parse(this.responseText);


//extra for retreving a node :) later on
//var nodetitle = json.title;
//var body = json.body.und[0].value;


     var csrf_header = "X-CSRF-Token: "+ json.token;

Ti.App.Properties.setString("csrf", json.token);
//use like xhr.setRequestHeader("X-CSRF-Token", Ti.App.Properties.getString("csrf"));

Ti.App.Properties.setString("Cookie", json.session_name +"="+ json.sessid);

//prob save the userdata to local db ?


loginview.add(createnodebutton);
        
     }else{
        alert('Error loging you in  Try again later. ' + this.status + " " + this.response);
     }          
    };
   
    xhr.onerror = function(e){
        alert('Error loging in. ' + e.error);
        };
 
    // now we have the token can log the user in
 
});


//add view to windoew

win.add(loginview);
win.open();



//create a node to test if this works

function creeateanode(){
// if developing on local host .. get your network ip cmd ipconfig
   
var serverURL = mydrupalrest +'node';
    
    var xhr=Titanium.Network.createHTTPClient();  
    xhr.setRequestHeader("X-CSRF-Token", Ti.App.Properties.getString("csrf"));
    xhr.setRequestHeader("Content-Type","application/json");
   // xhr.setRequestHeader("Cookie",Ti.App.Properties.getString("Cookie"));
   
    xhr.open("POST", serverURL);
   
    var node = {"node":{
"type":"page",
"title":"This is my new title ",
"language":"und",
"body":{"und":{"0":{"value":"This is the body of my node"}}}
}};
 
    var obj = (node);
   xhr.send(JSON.stringify(obj));
 
    xhr.onload = function(){
     //alert("responseText: " + this.responseText);
     if(this.status == '200'){
        alert('Node Creation successful!');
             
     }else{
        alert('Node creation failed. . ' + this.status + " " + this.response);
     }            
    };
 
    xhr.onerror = function(e){
      alert('Node creationerror: ' + e.error +" " + this.response);
      //alert("Cookie is "+Ti.App.Properties.getString("Cookie"));
       alert("X-CSRF-Token"+Ti.App.Properties.getString("csrf"));
     };
 
};



// logout

var logoutbutton = Ti.UI.createButton({
    title:'Log out',
    left:50,
    right:50,
    top:10,
    height: 35
});
loginview.add(logoutbutton);



//button click function
logoutbutton.addEventListener('click',function(e)
{
logout();
});



// create node ?
var createnodebutton = Ti.UI.createButton({
    title:'create node',
    left:50,
    right:50,
    top:10,
    height: 35
});

//button click function
createnodebutton.addEventListener('click',function(e)
{
  creeateanode();
});



function logout(){
  var logout_url = mydrupalrest + "user/logout";
   var xhr=Titanium.Network.createHTTPClient();  
    // alert(Ti.App.Properties.getString("csrf"));
    xhr.setRequestHeader("X-CSRF-Token", Ti.App.Properties.getString("csrf"));
    xhr.setRequestHeader("Content-Type","application/json");
    //xhr.setRequestHeader("Cookie",Ti.App.Properties.getString("Cookie"));
    xhr.open("POST", logout_url);
   xhr.send();
     xhr.onload = function(){
     alert("responseText: " + this.responseText);
    
     Ti.App.Properties.setString("csrf", '');
     Ti.App.Properties.setString("Cookie", '');
     //remove create node button
        loginview.remove(logoutbutton);
               
    };
 
    xhr.onerror = function(e){
     
       alert('Cant logout .. error: ' + e.error);};
};