Cross-protocol login control with JSONP in SharePoint

Overview

This article describes the specific problem of creating a control in SharePoint, which will be located on an HTTP page and securely signs in a user through an SSL connection using HTTPS. Although the solution uses SharePoint exclusively, it can be easily modified for ASP.NET environments.

The problem

With the requirement that the user data should transferred securely to the server, the idea of a postback becomes unfeasible. At that point one might come with the idea of an asynchronous and secure request that carries the user data.

Those of you have worked with AJAX, might have stumbled upon asynchronous requests with jQuery more than once. While this type of requests allows building very rich internet applications, most of them have a key restriction regarding protocols and domains. Common requests cannot retrieve data from a different domain than the one hosting the application, and the protocol for the request must also be the same as the one used to retrieve the page.

Knowing of these limitations, our mind is focused to those AJAX requests that allow cross-protocol calls.

JSONP at the rescue

JSONP is an extension to the well known standard JSON, where the added "P" stands for "Padding". The padding is usually a JavaScript function call (might be any JavaScript code too) that is added by the server as a prefix to the data returned. These are two requests and responses from a server in both, JSON and JSONP formats:

JSON request:

        http://example.com/planets?json=true&planetid=4

JSON response:

        {
            "id" : 4,
            "name" : "Mars",
            "satellites" : 2
        }

JSONP request:

        http://example.com/planets?jsonp=true&planetid=4&jsonpcallback=callthisfunction

JSONP response:

        callthisfunction({            
            "id" : 4,
            "name" : "Mars",
            "satellites" : 2
        })     

As you can see, the only differences between both requests, is the "jsonpcallback" parameter, which is used as a function call to wrap the data in the response. This parameter can actually have any name, but in order to make it work with jQuery, it needs to end with the suffix "callback".

So, when a web page has a script tag with the previous request as the source, JSONP comes to life

        <script type="text/javascript" src="http://example.com/planets?jsonp=true&planetid=4&jsonpcallback=callthisfunction"></script>

Script tags can bring scripts from any domain, using any protocol. Using the previous script tags, not only brings the data from the server, but also calls

the callback function, allowing the client to run any desired code.

 

Putting all together

Now that we know the power of JSONP, we can use it to design our solution. On the client side, we will use a web part that uses jQuery to perform the request. On the server side, we will have an ASP.NET handler (.ashx) that logs the user in, or returns an error if the user or password supplied are invalid.

Let's begin with the server side. To publish the handler from SharePoint, we need the ashx file to look something like this:

    <%@ Assembly Name="Microsoft.Web.CommandUI, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
    <%@ Assembly Name="MyNamespace", Version=1.0.0.0, Culture=neutral, PublicKeyToken=a39c10d65c32c9c1, processorArchitecture=MSIL" %> 
    <%@ WebHandler Language="C#" Class="MyNamespace.MyHandler" %>
 

For the code file, we will use the SharePoint method "SPClaimsUtility.AuthenticateFormsUser" to perform the login. But also, we have to return a JSONP

response indicating whether that method succeeded or not. Let's see the easiest way of doing it:

    public partial class MyHandler : IHttpHandler
    {
        public bool IsReusable
        {
            get { throw new NotImplementedException(); }
        }
 
        public void ProcessRequest(HttpContext context)
        {
            context.Response.ContentType = "text/html";
            context.Response.Headers.Add("Access-Control-Allow-Origin", "*");
            // Parse the JSON callback
            string callback = context.Request["callback"] ?? string.Empty;
            callback = Regex.Replace(callback, @"[^a-zA-Z0-9\?]", "");
            string user = context.Request["User"];
            string password = context.Request["Password"];
            if (SPClaimsUtility.AuthenticateFormsUser(context.Request.Url, user, password))
            {
                context.Response.Write(callback + "({ \"result\" : true });");
            }
            else
            {
                context.Response.Write(callback + "({ \"result\" : false });");
            }
        }
    }

From this code, you can see how we parse the "callback" parameter, parsing the basic characters on it, and we use it to send the response to the client, which is an object containing only the "result" boolean property.

Another thing interesting in this code is the "Access-Control-Allow-Origin" header, which filters the domains that can request data from this server. By using the character "*", we are allowing any domain to request data from our server.

Now, let's see the client side. When a user enters his information in the "txtUser" and "txtPassword" fields, this script does the trick:

        function onLogin(evt, message) {
            var user = $("#txtUser").attr("value");
            var password = $("#txtPassword").attr("value");
            $.getJSON(
                https://example.com/_layouts/MyNamespace/MyHandler.ashx?callback=?,
                {
                    User: user,
                    Password: password
                },
                function (data) {
                    if (data.result) {
                        // Login succeeded, redirect to home page:
                        window.location = "https://example.com/default.aspx"; 
                    }
                    else {
                        // Login failed, show error message without leaving the page:
                        document.getElementById('lblLoginResult').innerHTML = "Wrong Userid or Password";
                    }
                }
            );
        }

 

To perform a JSONP request in jQuery, we just perform a normal JSON request. jQuery automatically detects the string "callback=?" in the url and

performs the request as JSONP. The function we supplied as the callback, is called with a JavaScript object, which is parsed by jQuery from the response

sent by the server.

We now have both sides working. We have built a login control that works on every browser, protects the users' security, and efficiently uses

HTTP/HTTPS protocols.

 

Error handling

Although some of the errors can be seen on the browser's error console, there is no way to handle JSONP errors in JavaScript. This is one of the main drawbacks of the protocol. When an error occurs, it just fails silently.

 

Security concerns

As we saw, JSONP is a powerful tool that makes possible some scenarios that would otherwise be unfeasible. However, it should be used with extreme care, especially with cross domain requests. Remember that the padding is nothing more than JavaScript code that will be run by the user's browser. This gives the external server, the possibility to send a malicious script to start an attack.

 

Conclusions

We have seen JSONP in action, resolving a specific problem in a very simple way. However, we also saw the drawbacks of this approach. Like any other tool, it is useful to keep in mind how powerful it is, the kind of problems it addresses, and the weak points it presents.

 

Thanks for reading!

Alfonso Cora

No Comments