Displaying Cross-Domain/Secure Images from SharePoint Apps

One of my biggest frustrations in developing apps are the cross-domain challenges that inherently exist when we decouple apps from the platform(s) they needs to consume. There are a number of approaches for dealing with cross-domain data such as cross-origin resource sharing (CORS) and JSONP. However, images are trickier…especially when they require authentication. Internet Explorer Security Zones can also wreak havoc on images when domains are in different zones. In this post, I’ll illustrate a technique for dealing with these image challenges. The following video illustrates the concepts of this post:

[View:https://www.youtube.com/watch?v=5258FrBH_1c]

The Challenge

OAuth enables an application to consume 3rd party data/services without the knowledge of the user’s credentials to that 3rd party. It accomplishes this (at least the first time) through redirection to the 3rd party (where the user might get prompted to login) for tokens the app can use to retrieve data. Browsers are very efficient at handling location redirection to support OAuth, but HTML image elements are not. If the html image (in app domain) has its source is set to a 3rd party URL (image domain), cross-domain challenges can exist. The two biggest challenges exist when the image is secure (ie – requires authentication):

  • Browser is authenticated to the image domain, but the image domain is in a separate IE Security Zone that prevents the app domain from sharing the authentication cookie
  • Browser is NOT authenticated to the image domain. The app might have an access token to get data from image domain, but an HTML image element is not access token aware

Both these scenarios can result in broken images as seen in below. As app developers, we should assume that our apps will run in a different security zone as SharePoint AND that users of our apps will NOT use the “keep me signed in” option of Office 365.

Broken images are the result of the HTML image element not being able to automatically login to display the secured image. You can easily see this by monitoring the web traffic during the picture request as seen below using the F12 Developer Tools of Internet Explorer. In the sample below, an image in a provider-hosted app is attempting to display a picture in the AppWeb. You can see the original image request in the top row and then several redirects (HTTP 302)...ultimately to login.microsoftonline.com:

The Solution

Instead of introducing a cross-domain image source, my approach is to leverage a RESTful service to read the image bytes and return a base64 encoded representation of the image. Any modern browser can this as the source of an HTML image element:

URL vs Base64 Image Source

<!-- URL Image Source --><img src="https://somedomain/sites/site/library/picture.png" /><!-- base64 Image Source...truncated for readability --><img src="data:image/png;base64,%RkGAV...EmFjYgJH2" />

 

The image service will accept the details needed to locate the image (ex: site URL, folder, filename, etc) and the access token to set on the header when it performs a GET on the image. The actual code is surprisingly simple:

Get Image RESTful Service

[ServiceContract(Namespace = "Core.CrossDomainImagesWeb")][AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]public class ImgService{ [OperationContract] [WebGet] public string GetImage(string accessToken, string site, string folder, string file) { //make the request string url = String.Format("{0}_api/web/GetFolderByServerRelativeUrl('{1}')/Files('{2}')/$value", site, folder, file); HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest; request.Headers.Add("Authorization", "Bearer" + " " + accessToken); using (HttpWebResponse response = request.GetResponse() as HttpWebResponse) { using (var sourceSteam = response.GetResponseStream()) { using (var newStream = new MemoryStream()) { sourceSteam.CopyTo(newStream); byte[] bytes = newStream.ToArray(); return "data:image/png;base64, " + Convert.ToBase64String(bytes); } } } }}

 

The great part of this approach is that the service can be consumed server-side or client-side as seen in the code below:

Calling GetImage Server-side

//use ImgService to get the base64 representation of imageServices.ImgService svc = new Services.ImgService();Image2.ImageUrl = svc.GetImage(spContext.UserAccessTokenForSPAppWeb, spContext.SPAppWebUrl.ToString(), "AppImages", "O365.png");

 

Calling GetImage Client-side

//make client-side call for the third image in base64 formatvar appWebUrl = decodeURIComponent(getQueryStringParameter('SPAppWebUrl'));$.ajax({ url: '../Services/ImgService.svc/GetImage?accessToken=' + $('#hdnAccessToken').val() + '&site=' + encodeURIComponent(appWebUrl + '/') + '&folder=AppImages&file=O365.png', dataType: 'json', success: function (data) { $('#Image3').attr('src', data.d); }, error: function (err) { alert('error occurred'); }});

 

Here is a screenshot of a provider-hosted app that displays the same image from AppWeb three different ways…using a traditional absolute URL, a base64 encoded image rendered server-side, and a base64 encoded image rendered client-side. Notice the broken image of the absolute URL and successful rendering of the two base64 encoded images:

Conclusion

IE Security Zones and cross-domain images have been one of my biggest frustrations in app development. I hope that the solution outlined above can help you avoid similar frustration. Look for a complete sample of this solution in future release of the Office AMS project on CodePlex