SSO Token, Including SAP SSO2 and SiteMinder/Network Edge

As in basic authentication, the Datajs JavaScript library internally uses the XmlHttpRequest (XHR) object to handle the underlying HTTP or HTTPS requests/responses on the client.

From the XHR object’s API, Datajs uses setRequestHeader() and getAllResponseHeaders() to send and read the HTTP headers in the request and response.  For Single Sign-On and Network Edge authentication, issuers of SSO tokens, including SAP SSO2 logon tickets (MYSAPSSO2), as well as SiteMinder tokens (SMCHALLENGE, SMSESSION, and so on) normally use the “Set-Cookie” field in the HTTP header to send the token to the client, and expect the “Cookie” in the header to receive the token from the client. 

However, these specific headers are omitted from JavaScript access. See the W3C spec (http://www.w3.org/TR/XMLHttpRequest/). Instead, these headers are designed to be controlled by the user agent, in this case the browser control hosted by the Hybrid Web Container, to protect the client from rogue sites. According to the W3C spec it is the job of the user agent to support HTTP state management: to persist, discard, and send cookies, as received in the Set-Cookie response header, and sent in the Cookie header, as applicable. One possible exception allows cookie handling in JavaScript to set up a CORS request on the client and server, using the XHR’s “withCredentials” property.

Considering the reliance on the Hybrid Web Container-hosted browser control to handle the required SSO tokens, it is important to note the same origin policy surrounding automatic cookie management. That means from the client’s perspective, the domain from where the cookie-based token originates must be the same as where it needs to be redirected to access the protected OData endpoint, such as the SAP NetWeaver Gateway, while authenticated.  For the domain to be the same to the client, the URL pattern specifying transport protocol, servername, domain, and port number must match between token issuer and endpoint.  This should be possible using proxy mappings in the Relay Server or reverse proxy.

Regarding the SiteMinder component of Network Edge, its Policy Server supports a variety of authentication schemes, including Basic Authentication and HTML Forms-based Authentication.  The sample script below demonstrates an approach to handling a Basic 401 challenge from SiteMinder, as well as possible Forms authentication, involving HTTP status 302 indicating redirection. The script involving cookie handling is commented out and just informational, since this is managed by the user agent as described previously.

/**
* Sybase Hybrid App version 2.2
* 
* Datajs.SSO.js
* This file will not be regenerated, and it is expected that the user may want to
* include customized code herein.
*
* The template used to create this file was compiled on Mon Jul 9 19:54:04 CST 2012
* 
* Copyright (c) 2012 Sybase Inc. All rights reserved.
*/

// Capture datajs' current http client object.
var oldClient = OData.defaultHttpClient;

var sso_username = "";
var sso_password = "";
var sso_session = "";
var sso_token = "";

// Creates new client object that will attempt to handle SSO authentication, specifically SiteMinder login,
// in order to gain access to a protected URL.
var ssoClient = {
    request: function (request, success, error) {

        // For basic authentication, XMLHttpRequest.open method can take varUser and varPassword parameters.
        // If the varUser parameter is null ("") or missing and the site requires authentication, the
        // component displays a logon window. Although this method accepts credentials passed via parameter,
        // those credentials are not automatically sent to the server on the first request. The varUser and
        // varP	assword parameters are not transmitted unless the server challenges the client for credentials
        // with a 401 - Access Denied response. But SiteMinder may require additional steps, so save for
        // later...
        if (request.user != undefined && request.password != undefined) {
            sso_username = request.user;
            sso_password = request.password;
        }

        var onSuccess = function (data, response) {
            // Browser control will automatically cache cookies, with possible token, for next time, so
            // parsing Set-Cookie in HTTP response headers unnecessary here.
            //var setCookieHeader = response.headers["Set-Cookie"];
            //var setCookies = [];
            //parseSetCookies(setCookieHeader, setCookies);

            //for(var i=0; i < setCookies.length; i++)
            //{
            //	if (setCookies[i].substr(0, 9) === "SMSESSION")
            //		sso_session = setCookies[i];
            //	else if (setCookies[i].substr(0, 9) === "MYSAPSSO2")
            //		sso_token = setCookies[i];
            //}

            // Call original success
            alert("Calling original success");
            success(data, response);
        }

        var onError = function (sso_error) {
            if (sso_error.response.statusCode == 0) {
                // Attempt to parse error from response.body, e.g. sent from SAP NetWeaver as HTML page.
                if (sso_error.response.body.indexOf("401") != -1 &&
    				(sso_error.response.body.indexOf("Unauthorized") != -1 ||
    				 sso_error.response.body.indexOf("UNAUTHORIZED") != -1)) {
                    alert("SSO challenge detected");
                    sso_error.response.statusCode = 401;
                }
            }

            // Ensure valid response. Expecting either HTTP status 401 for SMCHALLENGE or 302 for redirection. 
            if (sso_error.response.statusCode != 401 &&
    		   sso_error.response.statusCode != 302) {
                alertText(sso_error.response.statusText);
                error(sso_error);
                return;
            }

            // 401 may include SMCHALLENGE=YES in Set-Cookie, so need to return along with Authorization
            // credentials to acquire SMSESSION cookie.
            if (sso_error.response.statusCode === 401) {
                // Browser control will automatically cache cookies, with possible token, for next time,
                // so parsing Set-Cookie in HTTP response headers unnecessary here.
                //var setCookieHeader = sso_error.response.headers["Set-Cookie"];
                //var setCookies = [];
                //parseSetCookies(setCookieHeader, setCookies);


                // Append existing headers.
                var newHeaders = [];
                if (request.headers) {
                    for (name in request.headers) {
                        newHeaders[name] = request.headers[name];
                    }
                }
                // Browser control should include SMCHALLENGE cookie.
                //newHeaders["Cookie"] = "SMCHALLENGE=YES";
                var enc_username = window.btoa(sso_username);
                var enc_password = window.btoa(sso_password);
                var basic_auth = "Basic " + enc_username + ":" + enc_password;
                newHeaders["Authorization"] = basic_auth;

                // Redo the OData request for the protected resource.
                var newRequest = {
                    headers: newHeaders,
                    requestUri: request.requestUri,
                    method: request.method,
                    user: sso_username,
                    password: sso_password
                };

                oldClient.request(newRequest, onSuccess, error);
            }

            // 302 indicates that the requested information is located at the URI specified in the Location
            // header. The default action when this status is received is to follow the Location header
            // associated with the response. When the original request method was POST, the redirected request
            // will use the GET method.
            if (sso_error.response.statusCode === 302) {
                // Get the redirection location.
                var siteminder_url = sso_error.response.headers["Location"];

                // Open a connection to the redirect and load the login form. 
                // That screen can be used to capture the required form fields.
                var httpRedirect = getXMLHTTPRequest();

                httpRedirect.onload = function () {

                    if (this.status < 200 || this.status > 299) {
                        alert("Error: " + this.status);
                        alertText(this.statusText);
                        error({ message: this.statusText });
                        return;
                    }

                    var sm_form_response = this.responseXML;
                    var siteminder_tags = {};

                    getSiteMinderTags(sm_form_response, siteminder_tags);

                    // Create the form data to post back to SiteMinder. Two ContentTypes are valid for sending
                    // POST data. Default is application/x-www-form-urlencoded and form data is formatted
                    // similar to typical querystring. Forms submitted with this content type are encoded as
                    // follows: Control names and values are escaped. Space characters are replaced by `+',
                    // reserved characters are escaped as described in [RFC1738], section 2.2:
                    // non-alphanumeric characters are replaced by `%HH', representing ASCII code of character.
                    // Line breaks are represented as CRLF pairs (i.e., `%0D%0A'). Control names/values are
                    // listed in order they appear in document. Name is separated from value by '=' and name/
                    // value pairs are separated from each other by `&'. Alternative is multipart/form-data.
                    //var formData = new FormData();
                    var postData = "";

                    for (var inputName in siteminder_tags) {
                        if (inputName.substring(0, 2).toLowerCase() === "sm") {
                            postData += inputName + "=" + encodeURIComponent(siteminder_tags[inputName]) + "&";
                            // formData.append(inputName, siteminder_tags[inputName]);
                        }
                    }
                    postData += "postpreservationdata=&";
                    postData += "USER=" + encodeURIComponent(sso_username) + "&";
                    postData += "PASSWORD=" + encodeURIComponent(sso_password);

                    // Submit data back to SiteMinder.
                    var httpLogin = getXMLHTTPRequest();

                    httpLogin.onload = function () {

                        if (this.status < 200 || this.status > 299) {
                            alert("Error: " + this.status);
                            alertText(this.statusText);
                            error({ message: this.statusText });
                            return;
                        }

                        // Browser control should cache required cookies so no need to parse HTTP response
                        // headers.
                        //var sm_cookie_response = this.response;
                        //var setCookieHeader = this.getResponseHeader("Set-Cookie");
                        //var setCookies = [];
                        //parseSetCookies(setCookieHeader, setCookies);

                        // Locate the URI to access next.
                        var newUrl = this.getResponseHeader("Location");

                        // Append existing headers.
                        var newHeaders = [];
                        if (request.headers) {
                            for (name in request.headers) {
                                newHeaders[name] = request.headers[name];
                            }
                        }
                        // Browser control should include SMSESSION cookie.
                        //newHeaders["Cookie"] = setCookieHeader;

                        // Redo the OData request for the protected resource.
                        var newRequest = {
                            headers: newHeaders,
                            requestUri: newUrl,
                            method: request.method,
                            user: sso_username,
                            password: sso_password
                        };

                        oldClient.request(newRequest, onSuccess, error);
                    }

                    httpLogin.open("POST", siteminder_url, true);
                    httpLogin.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
                    httpLogin.withCredentials = "true";
                    httpLogin.send(postData);
                    //httpLogin.send(formData);            	
                }

                httpRedirect.open("GET", siteminder_url, true);
                httpRedirect.responseType = "document";
                httpRedirect.send();

            }
        }

        // Call back into the original http client.
        var result = oldClient.request(request, success, onError);
        return result;
    }
};

// Parses Set-Cookie from header into array of setCookies.
function parseSetCookies(setCookieHeader, setCookies) {

    if (setCookieHeader == undefined)
        return;

    var cookieHeaders = setCookieHeader.split(", ");

    // verify comma-delimited parse by ensuring '=' within each token
    var len = cookieHeaders.length;
    if (len > 0) {
        setCookies[0] = cookieHeaders[0];
    }
    var i, j;
    for (i = 1, j = 0; i < len; i++) {
        if (cookieHeaders[i]) {
            var eqdex = cookieHeaders[i].indexOf('=');
            if (eqdex != -1) {
                var semidex = cookieHeaders[i].indexOf(';');
                if (semidex == -1 || semidex > eqdex) {
                    setCookies[++j] = cookieHeaders[i];
                }
                else {
                    setCookies[j] += ", " + cookieHeaders[i];
                }
            }
            else {
                setCookies[j] += ", " + cookieHeaders[i];
            }
        }
    }
}

// Parses response HTML document and returns array of INPUT tags.
function getSiteMinderTags(response, tags) {

    var inputs = new Array();
    inputs = response.getElementsByTagName("input");

    // get the 'input' tags
    for (var i = 0; i < inputs.length; i++) {
        var element = inputs.item(i).outerHTML;
        var value = "";

        // filter out inputs with type=button
        var stridex = element.indexOf("type=");
        if (stridex != -1) {
            var typ = element.substring(stridex + 5);
            stridex = typ.indexOf(' ');
            typ = typ.substring(0, stridex);

            if (typ.toLowerCase() === "button") {
                continue;
            }
        }

        stridex = element.indexOf("value=")
        if (stridex != -1) {
            value = element.substring(stridex + 6);
            stridex = value.indexOf(' ');
            value = value.substring(0, stridex);
        }

        tags[inputs.item(i).name] = value;
    }
}

function alertText(error) {

    var txt = JSON.stringify(error);
    alert("Error:\n" + txt);

    var length = txt.length;
    var sectionLength = 300;
    var index = Math.floor(length / sectionLength);
    for (i = 0; i <= index; i++) {
        var start = i * sectionLength;
        var end = (i + 1) * sectionLength;
        var segLength = sectionLength;
        if (end > length) segLength = length - start;
        alert(txt.substr(start, segLength));
    }
}


// Can either pass ssoClient explicitly, or set it globally for the page as the default:
OData.defaultHttpClient = ssoClient;
Related concepts
Basic Authentication
Related reference
Authentication Against an OData Source