Basic Authentication

The Datajs JavaScript library internally uses the XmlHttpRequest (XHR) object to handle the underlying HTTP or HTTPS requests/responses on the client. 

The XHR API’s open method optionally accepts user name and password credentials passed through parameters.  Likewise, the Datajs’ request object can take user and password members that map to those parameters. If credentials are not passed and basic authentication is required, the client is challenged with HTTP status 401.  If credentials are passed to the XHR object, internally it does not automatically send them on the first request. It submits the credentials only if challenged.  If this standard procedure is all that is required from the calling OData script, normally additional script can be avoided.  

The below sample script shows possible alternative approaches for handling a 401 status manually, or, in cases where the authentication needs to be centralized.

/**
* 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 reference
Authentication Against an OData Source
SSO Token, Including SAP SSO2 and SiteMinder/Network Edge