3

I have created a REST Web service using DataSnap in Delphi XE. I am calling server methods using the XMLHttpRequest JavaScript object. I am passing the username and password for authentication in the fourth and fifth optional parameters of the XMLHttpRequest Open method.

When I load my DataSnap server in Delphi and attach the debugger to IIS (I am using IIS 7.5) I can see that the first time I call one of the server methods results in the DSAuthenticationManager's OnUserAuthenticate event being called twice.

In the first call the user and password parameters of the OnUserAuthenticate event handler are empty strings. On the second call, the username and password that I passed in the XMLHttpRequest Open method appear in those parameters.

Once the user has been authenticated, any subsequent calls to any of the server methods using XMLHttpRequest results in a single call to the OnUserAuthenticate event handler, with the user's username and password appearing in the user and password parameters, respectively.

I have inspected the source code for a number of the DataSnap classes and have not found code that would cause this effect, which is leading me to think that this behavior may originate in the browser or IIS.

Why is OnUserAuthenticate being called twice upon the first server method call, and what is producing this effect?


In response to Mat DeLong's answer, I am showing one of generic functions I am using to call my server methods. This particular method makes a synchronous call. The baseURL parameter normally is a string similar to http://mydomain.com/myservice/myservice.dll/datasnap/rest/tservermethods1, and method might be myservermethod. Additional parameters are used to pass parameters to the server method:

function getJSONSync(baseURL, method) {
  var request = new XMLHttpRequest();
  var url = baseURL + method;
  for (var i= 2; i < arguments.length; i++) {
    url += "/" + arguments[i];
  }
  request.open("GET", encodeURI(url), false);
  request.send(null);
  if (request.status != 200) {
    throw new Error(request.statusText);
  } 
  return jQuery.parseJSON(request.responseText);
}

Edit: Lex Li appears to be correct, that IIS is only using basic authentication if it fails on the first request. Also, Mat is correct that the OnUserAuthenticate should only be called once per session. After examining the link he provided, it was apparent that I was failing to capture and return the session id. Here is an example of code that works, this time asychronously. It captures the sessionid and stores it in a hidden field. It then passes that session id back in each subsequent call in a Pragma header.

function getJSONAsync(callback, baseURL, method) {
  var request = new XMLHttpRequest();
  var timedout = false;
  request.onreadystatechange = function() {
     if (request.readyState !== 4) { return }
    if (timedout) { return }
    if (request.status >= 200 && request.status < 300)
    {
      callback(jQuery.parseJSON(request.responseText));
      //store the sessionid in a hidden field
      $("#sessionid").val(request.getResponseHeader('Pragma').split(',')[0].split("=")[1]);
    }
  }
  var url = baseURL + method;
  for (var i= 3; i < arguments.length; i++) {
    url += "/" + arguments[i];
  }
  request.open("GET", encodeURI(url), true);
  //pass the sessionid in the Pragma header, if available
  if (! $("#sessionid").val() == "") {
    request.setRequestHeader("Pragma", "dssession="+$("#sessionid").val());
  }
  //time out if request does not complete in 10 seconds
  var timer = setTimeout(function() { timedout = true; request.abort(); }, 10000);
  request.send(null);
}

Edit: When I first posted these two code samples I included sample usernames and passwords in the fourth and fifth parameters of the XMLHttpRequest open method. I was passing these values explicitly during testing. Passing the parameters like this is not recommended, so I removed those parameters in this edit. If you omit these passwords, and don't pass them using another methods (such as in the header of your first REST server method invocation) the browser will display a dialog box asking the user for this information. Once the username and password has been supplied, the browser will remember this information for the remainder of the session. If you close and then reopen the browser and invoke another REST server method, you will once again be challenged for the username and password. This assumes, of course, that you are rejecting invalid users through the OnUserAuthenticate event handler.

Cary Jensen
  • 3,751
  • 3
  • 32
  • 55

2 Answers2

3

Well, that's a typical IIS behavior for other technologies, such as ASP.NET.

The initial request does not contain your user credentials, and triggers a 401 response. Then the second request does send out your user credentials, then IIS can return 200.

Not sure if this applies to DataSnap, but if you capture network packets, you may find it out.

http://msdn.microsoft.com/en-us/library/system.net.httpwebrequest.preauthenticate.aspx

Lex Li
  • 60,503
  • 9
  • 116
  • 147
  • 1
    This appears to be correct. I examined the packets using wireshark. The first request does not include basic authentication header information, while the second request does. Thanks! – Cary Jensen Aug 06 '11 at 13:51
2

I can't say for sure why you are seeing this behaviour without seeing your server code, and the exact JavaScript you are using with the XHR. OnUserAuthenticate is intended to be called once ever, for a session.

You can see the XE DataSnap REST protocol here: http://docwiki.embarcadero.com/RADStudio/en/DataSnap_REST_Messaging_Protocol#Session_Information

It is intended that the first call returns a session ID, and all future calls are supposed to specify that. If this is done, all calls but the first use the session ID given to skip the authentication process and only go to the OnUserAuthorize event, if one is assigned.

If you are seeing OnUserAuthenticate called twice, that is likely a bug unless it is something weird in the XHR, or IIS configuration.

If you could provide a bit more detail I'd be happy to take a closer look.

Mat DeLong
  • 297
  • 4
  • 17
  • Mat: Thank you for the reply. I am calling the DataSnap server as a pure REST server without any of the proxy generated code. For example, I am using XMLHttpRequest directly (I am not using the proxy generated serverMethods() function). Used this way, OnUserAuthenticate is called once for each and every server method invocation (with the exception of being called twice on the first call from a browser session). I have edited my question and have added the XMLHttpRequest call. – Cary Jensen Aug 05 '11 at 19:54
  • Mat: I have the impression that I should be reading the session id from the response header, and returning it the headers of subsequent requests. I am not currently doing that, and will look into it. – Cary Jensen Aug 06 '11 at 14:23
  • Mat: Thanks again for the input and link. I modified my question to include an asychronous call that captures the sessionID and returns it in subsequent calls to the DataSnap server. Granted, this code does not check dssessionexpires Pragma value, but will simply capture a new sessionid if a new one is issued. Now, while there are two calls to OnUserAuthenticate (the first with no username or password, and the second with), no subsequent server method calls trigger OnUserAuthenticate. – Cary Jensen Aug 06 '11 at 18:08
  • I see you have the correct answer here, but I wanted to just mention one thing. The server will not issue you a new SessionId until you make a request without specifying a session Id. If you keep specifying an invalid sessionId, it will keep returning a session expired error, and never create a new session for you. – Mat DeLong Aug 10 '11 at 12:56