Thursday, June 13, 2013

More on Cross-domain Authentication

In my previous post I mentioned that it's impossible to do NTLM or Kerberos authentication with client side JavaScript in cross-domain requests. We know username and password can be used in JavaScript Ajax calls for BASIC authentication, but not for NTLM or Kerberos. However my above statement is not fully true since on top of JavaScript the browser could do some magic for you. Thanks my colleague Antonio for correcting me on this. He has a test environment with Kerberos setup where a test user logged on domainA can visit a page from http://companyB.com in domainB, and inside that page JavaScript Ajax calls successfully grab data from http://companyA.com in domainA using JSONP and Kerberos authentication without user typing in username and password. What he did is simply add http://companyA.com to Local Intranet zone in IE, then IE will automatically logon http://companyA.com with Kerberos authentication seamlessly.

In this post I will explore some common scenarios related to browser or AJAX authentication, including Intranet and Internet environment. Note that for those public sites and services that allow anonymous access the cross-domain authentication issue doesn't exist at all.

1. The browser is in the same domain as the server (Windows Authentication).

If the user is in the same domain as the server where the cross-domain requests go to, such as above Kerberos case, the single-sign-on (SSO) would work with NTLM or Kerberos authentication, i.e. IE would auto-authenticate to the server with the current logged in Windows account. No user input is required in this case but the server has to be added to the Local Intranet zone. The key point is the the "Automatic logon only in intranet zone" setting in Local Intranet zone:

How about other browsers? Chrome shares the same network configuration with IE, so the IE settings will apply to Chrome and the auto-logon would just work fine in Chrome. In Firefox you need to open up the system configuration by typing "about:config" in the address bar, then double click the "network.automatic-ntlm-auth.trusted-uris" entry and set the domain names you want to auto-authenticate to, e.g. "google.com, microsoft.com" (not in "*.google.com" format which is used in IE trusted sites).

If the authentication failed, the browser will popup a login window to let you input your username and password.

2. The browser is not in the same domain as the server (Windows Authentication).

If the user is not in the same domain as the server where the cross-domain requests go to, e.g. a user logs into domainA and accesses a web page in which some JavaScript Ajax calls to http://companyB.com in domainB, then the single-sign-on won't work because domainA users can not be authenticated by domainB so a login window will pop up for user to input the username and password. In IE you have the option to save the password in this case:

After you select to save your password then next time IE will automatically authenticate for you (no need to input username and password again) on the same site if the "Automatic logon with current user name and password" is configured for the zone:

You can only save the password in IE but not in Chrome or Firefox. When the password is saved the Chrome will have the same behavior as IE, and you need to configure the same "network.automatic-ntlm-auth.trusted-uris" setting to let Firefox do the auto-authentication.

Where the password is saved by IE? They are stored in Windows secure storage. You can see and update those saved usernames and passwords by clicking "Control Panel => User Account => Advanced => Manage Password" or run "control keymgr.dll" command:

Just like the previous case you will also see a browser login window if the authentication failed.

3. Server is Form authentication.

In Form authentication you submit your username and password to the server. The server will issue an authentication cookie to the client if username and password are valid, then you are authenticated and logged on. The interactive steps are purely HTML and all browsers have the same behavior. After logging in the authentication cookie will be sent by browser in the consequent calls to the same server with same URI domain. The auto-authentication of AJAX calls will be okay if the cookie is valid which means that you have logged in to the server before the AJAX call. In some login forms you can select to "keep me logged in" option where a persistence cookie is generated for long time usage so you can auto-login next time even you close and reopen the browser.

Not like Windows authentication, browser will not popup a login window when authentication failed in AJAX calls, instead any error is silently swallowed by the browser by default. In Form authentication the server will respond to the client to redirect to the login page which is not recognized by AJAX call. Of course you can handle that specific 302 redirect case and popup a login window using JavaScript but that's another story.

4. How about OpenID, SAML, OAuth, Claimed based authentication, Federated authentication...?

In most cases the behavior is the same as Form authentication: access would be okay if you have signed in before the AJAX calls. This is valid as long as the server is using cookie to trace the authentication status, such as Windows and Form authentication discussed above, otherwise the authentication immediately fails. How to know if the server is using authentication cookie? You can check if you are able to access the site assets after successfully authenticated through the browser.

There're some cases where each service request requires authentication. The AJAX call will fail in such case unless you provide required data. One example is the new Twitter API V1.1: every Twitter API service call now requires OAuth authentication. Setting OAuth header using JavaScript directly is impractical and unsafe (however you could calculate the OAuth signature from the web server and JavaScript simply pass it to the Twitter service).

Also like Form authentication, you won't see any popup window whenever the authentication fails, and the AJAX calls are simply failed.

5. What about SSL, SSL client authentication?

SSL, also known as https protocol, encrypts the data in transport layer so the communication between the client and the server is protected. SSL itself doesn't do anything for Authentication. However server certificate is required to establish a SSL connection. The AJAX calls will fail if the server certificate is invalid such as a self-signed certificate. Browser will display a warning message if you visit the server with invalid certificate directly (not from AJAX call):

In order to get rid of the warning message, you could install the certificate and put it into Trusted Root Certification Authorities for IE and Chrome (caution of doing so) or import it to Firefox. After that you can visit the site without seeing the warning message page, and the AJAX calls now will be able to go through to the server for authentication.

SSL client authentication is another story. Basically SSL server also asks client's certificate when setting up the SSL connection. The SSL client authentication can be configured as optional or required. If client authentication is required the SSL connection will fail if browser doesn't provide a certificate, or the certificate is invalid, or the certificate is not accepted. If the SSL client authentication is okay, the SSL connection will be established and the regular server side authentication will come to play in the next step.

In summary, an indicator of a successful AJAX call is that you can browse the resource or service directly without any issue. The statement is okay for same-domain AJAX calls, but that's not enough in cross-domain scenarios where you can visit the site successfully in the browser but the cross-domain AJAX calls fail (such failure is not caused by client-server authentication). Cross-domain AJAX communication requires special implementation in both client side and server side, for example, you can define {crossDomain: true, dataType: 'jsonp'} parameters for jQuery AJAX calls in the client side, and implement JSONP or CORS in the server side. Following html page is created to test the cross-domain request using jQuery. Simply save the html page in your local machine, open the page and run the test:

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />          
    <title>Cross-domain AJAX test</title>          
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js" ></script>
</head>     
<body>         
    <div>
        <div style="text-align:center">
            <h3>Cross-domain AJAX test</h3>
            Server URL:  <input name="txtUrl" type="text" id="txtUrl" style="width:500px;" />
            <button id="btnTest">Send cross-domain request</button>
        </div>
        <div id="result"></div>
    </div>
    <script type="text/javascript">
        $(document).ready(function() {
            $("#btnTest").click(function () {
                testRequest($("#txtUrl").val());    
            });
        });
        
        function testRequest(url) {
            $("#result").html("");
            if (url) {
                $.ajax({url: url, cache: false, crossDomain: true, dataType: 'jsonp'})
                .done(function(data) {
                    var value = getDataString(data);//decodeURIComponent($.param(myObject));
                    $("#result").html("Response:<pre>" + value + "</pre>");})
                .fail(function(xhr, err) {
                    $("#result").html("Error occurs.");
                });    
            } else {
                $("#result").html("Invalid Url.");
            }
        }
        
        // http://www.sitepoint.com/javascript-json-serialization/
        function  getDataString(obj) { 
            var t = typeof (obj);
            if (t != "object" || obj === null) {
                // simple data type
                if (t == "string") 
                    obj = '"'+obj.replace(/>/g,'&gt;').replace(/</g,'&lt;')+'"';
                return String(obj);
            } else {
                // recurse array or object
                var n, v, json = [], arr = (obj && obj.constructor == Array);
                for (n in obj) {
                    v = obj[n]; t = typeof(v);
                    if (t == "string") 
                        v = '"'+v+'"';
                    else if (t == "object" && v !== null) 
                        v = getDataString(v);
                    json.push((arr ? "" : '"' + n + '":') + String(v));
                }
                return (arr ? "[" : "{") + String(json) + (arr ? "]" : "}");
            }
        };
    </script>
</body>
</html>