Friday, September 10, 2010

Single Sign-on for Cross Domain Applications

In general, web authentication puts a cookie in your browser that says, "I've authenticated with you, and I am who I say I am." This is works great until you have parts of your application that exist on another domain; or even multiple applications for that matter. The system breaks down in this scenario because a cookie put into the browser is only accessible to the domain (and sub domains) that created it.

For example, if you have www.MyServiceA.com and you login there, it puts a cookie into your browser that says you've authenticated. When you visit other parts of www.MyServiceA.com, they see the cookie and give you access. If www.MyServiceA.com links you to www.MyServiceB.com, the latter site will not have access to the cookie created for www.MyServiceA.com and will force you to login again.

The Basic Approach
One method of doing this requires several redirects and passing the cookie info in a URL string.


At step 1, the user logs into mail.com/login and gets the authentication token xyz. The user then clicks a link that takes them to calander.com/default (step 2), which sends back a redirect to a special page on mail.com that gets the authentication cookie and sends it back in the URL's query string (step 3). The redirect from step 3 sends the browser to a special page on calander.com that looks at the query string to determine the cookie value and sets the cookie in the browser accordingly (step 4), redirecting the user back to the original page at calander.com requested in step 2.

A Simpler Approach
This solution works, but when I first saw it I thought there had to be a better way. Upon further research, I found that you can actually simply things by combining steps 3 and 4 into an HTML img tag. The silence and blank expression on your face tell me you need more explanation.

The trick, is to include an image tag that has its source set to the special setCookie URL at calander.com, passing the current authentication token in the query string part of the URL. This way when the browser renders the image on the mail.com page, it will hit the URL (which has the authentication token in its query string), which will be giving the cookie value to the other domain (calander.com). The other domain can then take the cookie value and tell the browser to set that cookie for the calander.com domain. Now when the user navigates to calander.com the cookie will be available and the user can be considered authenticated.

<html>
    <head>
    </head>
    <body>
        <script src="scripts/jquery-1.3.2.min.js" type="text/javascript"></script>
        <script type="text/javascript">
            $(document).ready(function () {
                var singleSignOnUrl = "<%= this.SingleSignOnUrl %>?c=<%= this.SingleSignOnToken %>";

                //
                // Add an image element with the source set to the special cookie
                // setter URL. Since the URL is not responding with a valid image,
                // the element's onload event will not fire. Instead, the onerror
                // event will be fired after the special URL finishing processing.
                //
                $("#img").attr("src", singleSignOnUrl));
            });

            function onError() {
                    $("#lblMessage").text("Cookie in the other domain has been set!");
                    // Make link to calander.com available
            }
        </script>        
  <img id="img" style="display: none;"></img>
  <span id="lblMessage">Setting cookie...</span>
    </body>
</html>

The example above assumes you have an aspx page with SingleSignOnUrl and SingleSignOnToken properties on the page that return the special calander.com/setCookie and authentication cookie value respectively. The setCookie page could be an http handler (setCookie.ashx) that does something like this:

///////////////////////////////////////////////////////////////////////////
    /// 
    /// Handler that looks for a specific query string parameter and reflects
    /// it back as a cookie.
    /// 
    ///////////////////////////////////////////////////////////////////////////
    public class SingleSignOnHttpHandler : IHttpHandler
    {
        ///////////////////////////////////////////////////////////////////////////
        /// Key used to represent the single sign on token.
        ///////////////////////////////////////////////////////////////////////////
        public const string AuthenticationTokenKey = "authentication-token";

        ///////////////////////////////////////////////////////////////////////////
        /// 
        /// Initializes a new instance of the  class.
        /// 
        ///////////////////////////////////////////////////////////////////////////
        public SingleSignOnHttpHandler()
        {
        }

        #region IHttpHandler Members

        ///////////////////////////////////////////////////////////////////////////
        /// 
        /// Gets a value indicating whether another request can use the  instance.
        /// 
        /// 
        /// true if the  instance is reusable; otherwise, false.
        /// 
        ///////////////////////////////////////////////////////////////////////////
        public bool IsReusable
        {
            get { return true; }
        }

        ///////////////////////////////////////////////////////////////////////////
        /// 
        /// Enables processing of HTTP Web requests by a custom HttpHandler that implements the  interface.
        /// 
        /// An  object that provides references to the intrinsic server objects (for example, Request, Response, Session, and Server) used to service HTTP requests.        ///////////////////////////////////////////////////////////////////////////
        public void ProcessRequest(HttpContext context)
        {
            //
            // Take the appropriate query string parameter and reflect it back as a cookie.
            //
            context.Response.ContentType = "text";
            context.Response.AddHeader("p3p", @"CP=""NON ADM OUR""");

            string value = context.Request.QueryString["c"];
            if (value.IsValid())
            {
                context.Response.SetCookie(new HttpCookie(AuthenticationTokenKey, value));
            }
        }

        #endregion
    }

The p3p header information is necessary for compatibility with Internet Explorer. Without that header, IE would ignore the returned cookie.

At this point, once the user accesses the page at mail.com with the hidden image, the users's browser will have the authentication cookie set in both the mail.com and calander.com domains, which will allow them to navigate between the two domains without having to authenticate.

2 comments:

  1. I'm playing around with comment frames because they were taking too much space, even though the extra white did make code easier to read. For methods, I only use the top line now. For code comments I've removed both top and bottom, although I might re-add the top one.

    ReplyDelete
  2. Nice post.Thank you for taking the time to publish this information very useful!I’m still waiting for some interesting thoughts from your side in your next post thanks.
    purchase domain name

    ReplyDelete