Monday, June 17, 2013

Proxy Page for New Twitter API 1.1

The old Twitter API V1.0 is just retired. The new API V1.1 requires OAuth authentication for each Twitter request. Our SharePoint Twitter WebPart is no longer working since this API update. I created a SharePoint layout page as a proxy connecting to Twitter using the new API so the Twitter services are available in our Intranet without the troublesome OAuth authentication. Also the proxy page caches the result locally for performance consideration and avoids exceeding the new Twitter request limit:

<%@ Page Language="C#"  %>

<%@ Import Namespace="System.Web.UI" %>
<%@ Import Namespace="System.Collections.Generic" %>
<%@ Import Namespace="System.Globalization" %>
<%@ Import Namespace="System.Security.Cryptography" %>
<%@ Import Namespace="System.Net.Security" %>
<%@ Import Namespace="System.Net" %>
<%@ Import Namespace="System.IO" %>
<%@ Import Namespace="System.Text" %>

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>A proxy page for Twitter API V1.1</title>
    <meta name="ROBOTS" content="NOINDEX, NOFOLLOW">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <meta http-equiv="Content-language" content="en">

    <script runat="server">
            
        // oauth application keys
        string consumerKey = "xxxxxxxx";
        string consumerSecret = "xxxxxxxx";
        string accessToken = "xxxxxxxx";
        string accessTokenSecret = "xxxxxxxx";

        int cacheTime = 5; // Cache time in minutes
        string baseUrl = "https://api.twitter.com/1.1/";        
                
        void Page_Load(object sender, System.EventArgs e)
        {
            // handle parameters
            bool noCache = Request.QueryString["nocache"] == "true" ? true : false;
            string endpoint= string.IsNullOrEmpty(Request.QueryString["endpoint"]) ? "statuses/user_timeline.json" : Request.QueryString["endpoint"];
            string[] resStrings = new string[] { "endpoint", "nocache", "callback", "_" };
            List<string> reservedParameters = new List<string>(resStrings); 
            
            Dictionary<string, string> parameters = new Dictionary<string, string>();
            StringBuilder keys = new StringBuilder();
            foreach (String key in Request.QueryString.AllKeys)
            {
                if (!reservedParameters.Contains(key) && !parameters.ContainsKey(key)) 
                {
                    parameters.Add(key, Request.QueryString[key]);
                    keys.Append(key + "=" + Request.QueryString[key] + "|");
                }
            }
            string cacheKey = keys.ToString();
            if (string.IsNullOrEmpty(cacheKey)) // simply return if no parameter provided 
            {
                lblInfo.Text = "Invalid parameters";
                return;            
            }     
            
            string tweets = Convert.ToString(Cache[cacheKey]);
            if (noCache || string.IsNullOrEmpty(tweets)) // check if cache exsits
            {
                string requestUrl = baseUrl + endpoint;
                try
                {
                    tweets = GetTweets(requestUrl, parameters);
                    // Update cache
                    Cache.Insert(cacheKey.ToString(), tweets, null, DateTime.Now.AddMinutes(cacheTime), System.Web.Caching.Cache.NoSlidingExpiration);
                }
                catch (Exception ex)
                {
                    lblInfo.Text = "Error occur: " + ex.Message;
                    return;
                }
            }
            
            // prepare for writting data to Response
            Response.Clear();
            Response.ContentType = "application/json; charset=utf-8";
            Response.ContentEncoding = System.Text.Encoding.UTF8;
            if (!string.IsNullOrEmpty(Request.QueryString["callback"])) // wrap data for JSONP
                Response.Write(string.Format("{0}({1})", Request.QueryString["callback"], tweets));
            else
                Response.Write(tweets);
            Response.End();
        }

        // Reference: https://dev.twitter.com/discussions/15206
        string GetTweets(string url, Dictionary<string, string> parameters)
        {
            string responseString = string.Empty;
            StringBuilder queryStrings = new StringBuilder();

            // OAuth setting
            string oauthSignatureMethod = "HMAC-SHA1";
            string oauthVersion = "1.0";
            string oauthNonce = Convert.ToBase64String(new ASCIIEncoding().GetBytes(DateTime.Now.Ticks.ToString()));
            TimeSpan timeSpan = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
            string oauthTimestamp = Convert.ToInt64(timeSpan.TotalSeconds).ToString();
            string compositeKey = Uri.EscapeDataString(consumerSecret) + "&" + Uri.EscapeDataString(accessTokenSecret);

            // OAauth signature: https://dev.twitter.com/docs/auth/creating-signature
            string oauthSignature;
            SortedList<string, string> authSigBaseValues = new SortedList<string, string>();
            authSigBaseValues.Add("oauth_consumer_key", consumerKey);
            authSigBaseValues.Add("oauth_nonce", oauthNonce);
            authSigBaseValues.Add("oauth_signature_method", oauthSignatureMethod);
            authSigBaseValues.Add("oauth_timestamp", oauthTimestamp);
            authSigBaseValues.Add("oauth_token", accessToken);
            authSigBaseValues.Add("oauth_version", oauthVersion);
            foreach (string key in parameters.Keys)
            {
                string escapedKey = Uri.EscapeDataString(key);
                string escapedValue = Uri.EscapeDataString(parameters[key]);
                authSigBaseValues.Add(escapedKey, escapedValue);
                queryStrings.Append("&" + escapedKey + "=" + escapedValue);
            }

            // build signagure base string
            StringBuilder oauthSigSB = new StringBuilder();
            foreach (KeyValuePair<string, string> item in authSigBaseValues)
            {
                oauthSigSB.Append("&" + item.Key + "=" + item.Value);
            }
            string signatureBaseString = "GET&" + Uri.EscapeDataString(url) + "&" + Uri.EscapeDataString(oauthSigSB.ToString().Remove(0, 1));

            // create OAuth signature 
            using (HMACSHA1 hasher = new HMACSHA1(ASCIIEncoding.ASCII.GetBytes(compositeKey)))
            {
                oauthSignature = Convert.ToBase64String(hasher.ComputeHash(ASCIIEncoding.ASCII.GetBytes(signatureBaseString)));
            }

            // create the request header
            string headerFormat = "OAuth oauth_nonce=\"{0}\", oauth_signature_method=\"{1}\", oauth_timestamp=\"{2}\", " +
                    "oauth_consumer_key=\"{3}\", oauth_token=\"{4}\", oauth_signature=\"{5}\", oauth_version=\"{6}\"";
            string authHeader = string.Format(headerFormat,
                                    Uri.EscapeDataString(oauthNonce),
                                    Uri.EscapeDataString(oauthSignatureMethod),
                                    Uri.EscapeDataString(oauthTimestamp),
                                    Uri.EscapeDataString(consumerKey),
                                    Uri.EscapeDataString(accessToken),
                                    Uri.EscapeDataString(oauthSignature),
                                    Uri.EscapeDataString(oauthVersion)
                            );
            if (queryStrings.Length > 0)
                url = url + "?" + queryStrings.ToString().Remove(0, 1);
            ServicePointManager.ServerCertificateValidationCallback = 
                new RemoteCertificateValidationCallback(delegate { return true; });
            ServicePointManager.Expect100Continue = false;

            // create request
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
            request.Headers.Add("Authorization", authHeader);
            request.Method = "GET";
            request.ContentType = "application/x-www-form-urlencoded";

            try
            {
                WebResponse response = request.GetResponse();
                responseString = new StreamReader(response.GetResponseStream()).ReadToEnd();
            }
            catch (Exception ex)
            {
                throw  ex;
            }

            return responseString;
        }
    </script>

</head>
<body>
    <form id="form1" runat="server">
    <div>
        <p>
            <asp:Label ID="lblInfo" runat="server"></asp:Label>
        </p>
    </div>
    </form>
</body>
</html>

The proxy page can work as a regular ASP.NET page inside IIS. It can also run as a layout page in SharePoint: simply copy the file to the layouts folder under SharePoint 12/14 hive. and it will just work.

The proxy accepts different endpoint and parameter where the endpoint is the twitter service endpoint such as "statuses/user_timeline.json" or "search/tweets.json", and parameter is the query strings you pass to twitter such as "screen_name=mytwittername". The easiest way to consume the proxy Twitter servcie is using JavaScript which can be inserted into SharePoint OOB content editor WebPart:

<script type="text/javascript">
<!-- 
    $(document).ready(function() {
        // twitter proxy URL
        var url = 'http://twitterproxy/twitter.aspx?endpoint=statuses/user_timeline.json&screen_name=myname&count=10';
       // http://twitterproxy/twitter.aspx?endpoint=search/tweets.json&q=from:myname&result_type=recent
        $.ajax({url : url, cache: false, crossDomain: true, dataType: 'jsonp'}).done(function(data) {
            $('#tweeters').html("");
            $.each(data, function(i, tweet) {
                if(tweet.text) {
                    var date = parseDate(tweet.created_at);
                    var tweet_html = '<div><div class="tweetText">' + tweet.text + '</div>';
                    tweet_html += '<div class="tweetDate">'+ date.toString().substring(0, 24) +'</div></div>';
                    $('#tweeters').append(tweet_html);
                }
            });
        });
    });
    
    // Fix IE DateTime format issue
    function parseDate(str) {
        var v=str.split(' ');
        return new Date(Date.parse(v[1]+" "+v[2]+", "+v[5]+" "+v[3]+" UTC"));
    }
// -->  
</script>

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>

Thursday, June 06, 2013

Using FusionCharts in SharePoint

FusionCharts provide a separate product for SharePoint called Collabion Charts for SharePoint a SharePoint, where you can build charting pages easily in SharePoint by adding those charting WebParts. You have options to show FusionCharts in SharePoint if you have already purchased FusionCharts license and you don't want to pay extra for Collabion Charts. First you can use SharePoint as hosting environment, put all FusionCharts assets and the charting pages into a document library. Everything should just work fine. You can also add FusionCharts to a WebPart using out-of-box Content Editor WebPart (CEWP). First reference the FusionCharts JavaScript in one CEWP (edit HTML Source):
    <script src="/Documents/FusionCharts/FusionCharts.js" type="text/javascript"></script>
Then you can add chart in another CEWP:
    <div><div id="testChartContainer"></div></div>
    <script type="text/javascript">
    <!--  
        FusionCharts.setCurrentRenderer('javascript'); 
        var testChart = new FusionCharts( { id: "chart1", type: "Column2D", width: "400", height: "250"} );  
        testChart.setXMLUrl("/Documents/testChart.xml");      
        testChart.render("testChartContainer");
    // -->      
    </script>



Note: SharePoint 2010 could change the content you added to the CEWP via HTML Source editor. You need to make sure that the updated content and script are still valid.

Monday, June 03, 2013

A JavaScript Charting Solution - FusionCharts

Recently I have been working on FusionCharts to build dashboard pages. I think it's worthy writing some notes of my experience of using FusionCharts.

First of all, FusionCharts customer services are very good. I downloaded a trial version and did some testing. Then I have some issues and questions to ask. I always got their reply on the same day. FusionCharts is an India company. A few times I even received their replies in the afternoon which is midnight there (I am in UTC-5 zone). Overall they are helpful and respond quickly even although I am not their customer yet.

Secondly FusionCharts are purely client site implementation using Flash and JavaScript. All my discussion in this article is based on their JavaScript solution. That's different from some other data visualization tools I used before, such as Dundas, Infragistrics and DevExpress, all of which are .NET components, and they are considered as server side solution where you build your charts in ASP.NET code behind, and the charting images are generated in the server side.

The good part of client side approach is easy to develop and deploy. You simply create a HTML page, add desired FusionCharts objects, set their data source using JavaScript then you are done. The chart page can be put to anywhere that the user can access to such as in a web server, a shared drive or even local box, as long as the data source is also accessible to the user. In addition the client side JavaScript is considered as a cross-platform solution being able to run in different environment.

The drawbacks of client side approach include the potential performance issue, and the cross-domain data source restriction, as discussed below in detail.

Performance Issue

The charts are populated and rendered inside the browser. The performance could be an issue if you have many charts on a page. One simple dashboard test page with 10 charts consuming static content takes 3-5 seconds to render in latest Firefox and Chrome, and IE8/9 are even worst taking a least double time to render the page. Note that the tests are conducted locally so in reality the network round-trip would introduce some more delay. This may not be unacceptable in some cases. One suggestion from FusionCharts to improve the performance is wrap the charting JavaScript inside a setTimeout function and that would make the page a little bit more responsive during the page initial load:

    setTimeout(function () {
        var myChart= new FusionCharts( { id: "chart1", type: "Column2D", width: "400", height: "250"} );  
        myChart.setXMLUrl("chart1.xml");      
        myChart.render("chart1"); 
    }, 0);

Cross Domain Data Source Issue

FusionCharts support XML or JSON data source. The chart data can be embedded inside the page, static file or dynamically loaded from the server. There will be a cross-domain issue if the data source is not coming from the same domain as the original one hosting the page. For instance, if the chart page is from http://companyA.com, then all data from http://companyB.com will be rejected by the browser.

Recent version of Firefox and Chrome as well as IE10 support CORS standard to resolve the cross-domain issue. Basically the response from http://companyB.com includes some specific headers informing the browser to accept this cross-domain data. For above example the header "Access-Control-Allow-Origin: http://companyA.com" or "Access-Control-Allow-Origin: *" would work. Another generic solution for cross-domain requests is use JSONP. The client sends a request with a callback function name and the server wraps all the data as a parameter for that callback function.

Following code snippet demos how to implement CORS and JSONP in .NET:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Text;

namespace DataService
{
    /// <summary>
    /// Web handler to serve FusionCharts data using JSONP or CORS to resolve cross-domain issue 
    /// </summary>
    public class FusionChartService : IHttpHandler
    {

        public void ProcessRequest(HttpContext context)
        {
            string data = GetData(); // Populate chart data
            bool isJsonFormat = false; // Are data JSON format?
            
            context.Response.ContentEncoding = System.Text.Encoding.UTF8;
            context.Response.ContentType = "text/plain";

            if (string.IsNullOrEmpty(callback)) // Add CORS headers if NOT JSONP
            {
                context.Response.AppendHeader("Access-Control-Allow-Origin", "*");
                context.Response.AppendHeader("Access-Control-Allow-Headers", "Content-Type");
                string requestHeaders = context.Request.Headers["Access-Control-Request-Headers"];
                if (!String.IsNullOrEmpty(requestHeaders))
                    context.Response.AppendHeader("Access-Control-Allow-Headers", requestHeaders);
                context.Response.Write(data);
            }
            else // Wrap data inside the callback founction for JSONP
            {
                context.Response.Write(string.Format("{0}({1})", callback, isJsonFormat ? data : EscapeString(data)));
            }
        }

        private static string EscapeString(string s)
        {
            StringBuilder sb = new StringBuilder();
            sb.Append("\"");
            foreach (char c in s)
            {
                switch (c)
                {
                    case '\"':
                        sb.Append("\\\"");
                        break;
                    case '\\':
                        sb.Append("\\\\");
                        break;
                    case '\b':
                        sb.Append("\\b");
                        break;
                    case '\f':
                        sb.Append("\\f");
                        break;
                    case '\n':
                        sb.Append("\\n");
                        break;
                    case '\r':
                        sb.Append("\\r");
                        break;
                    case '\t':
                        sb.Append("\\t");
                        break;
                    default:
                        int i = (int)c;
                        if (i < 32 || i > 127)
                            sb.AppendFormat("\\u{0:X04}", i);
                        else
                            sb.Append(c);
                        break;
                }
            }
            sb.Append("\"");

            return sb.ToString();
        }

        public bool IsReusable
        {
            get
            {
                return false;
            }
        }
    }
}
An example of Ajax call consuming the JSONP service using jQuery (testing with version of 1.8.3):
    var params = { url: 'http://companyB.com/FusionChartService.ashx', cache: false, crossDomain: true, dataType: 'jsonp text'};
    $.ajax(params).done(function(data) {
        var myChart = new FusionCharts( { id: "chart1", type: "Column2D", width: "400", height: "250", debugMode : false } );  
        myChart.setXMLData(data);      
        myChart.render("chart1"); 
    }).fail(function(xhr, err) {
        console.log(err);
    }); 

Cross Domain Authentication Issue

Cross-domain authentication is the major challenge for client side JavaScript solution. Windows and Kerberos authentication simply don't work. There's not much you can do with JavaScript in terms of cross-domain authentication. You can either enable anonymous access or implement BASIC authentication in the server. In the latter case, the client needs to pass plain text username and password for each cross-domain Ajax call which is not safe and is not allowed in most organizations. One workaround is use proxy server to authenticate the service. But that's not a flexible solution as that's hard to implement and maintain, introducing extra layer of delay, and pushing back some logic to the server. So FusionCharts is not recommended if do have such cross-domain use case.

2013-06 Update: More on cross-domain authentication.