Building File Handler Add-ins for Office 365
Microsoft recently announced the general availability of file handler add-ins for Office 365. This add-in type enables Office 365 customers to implement custom icons, previews and editors for specific file extensions in SharePoint Online, OneDrive for Business, and Outlook Web App (OWA). In this post, I’ll outline a solution for creating a file handler add-in for .png images. The add-in will allow .png images to be opened in an in-browser editor (think browser-based paint) for drawing/annotation/whiteboarding.
[View:https://www.youtube.com/watch?v=4lCVWqj2EUE]
Azure AD and Add-ins
Until now, Azure AD applications were considered stand-alone in the context of Office 365. File handler add-ins are similar to a stand-alone Azure AD web application, but are dependent on contextual information passed when invoked from Office 365 (more on that later).
The file handler add-in can be provisioned in Azure AD using the Connected Service Wizard in Visual Studio or manually through the Azure Management Portal or New Getting Started experience. However, file handler add-ins require a new permission that are currently only surfaced through the Azure Management Portal. This new permission is Read and write user selected files (preview) using the Office 365 Unified API. This permission allows a 3rd party file handler add-in to get access only to the files the user select (vs. ALL of the users files).
Once the application is registered, some additional configuration needs to be performed that the Azure Management Portal doesn’t (yet) surface in the user interface. Azure needs to know the extension, icon url, open URL, and preview URL for the file handler. This can be accomplished by submitting a json manifest update using Azure AD Graph API queries (outlined HERE). However, the Office Extensibility Team created an Add-in Manager website that provides a nice interface for making these updates. It allows you to select an Azure AD application and register file handler add-ins against it. Below, you can see the file handler registration for my .png file handler.
When the file handler add-in is invoked, the host application (SharePoint/OneDrive/OWA) posts some form details to the application. This includes the following parameters.
- Client - The Office 365 client from which the file is opened or previewed; for example, "SharePoint".
- CultureName - The culture name of the current thread, used for localization.
- FileGet - The full URL of the REST endpoint your app calls to retrieve the file from Office 365. Your app retrieves file using a GET request.
- FilePut - The full URL of the REST endpoint your app calls to save the file back to Office 365. You must call this with the HTTP POST method.
- ResourceID - The URL of the Office 365 tenant used to get the access token from Azure AD.
- DocumentID - The document ID for a specific document; allows your application to open more than one document at the same time.
If the application (which is secured by Azure AD) isn’t authenticated, it will need to perform an OAuth flow with Azure AD. This OAuth flow (which includes a redirect) will cause the application to loose these contextual parameters posted from Office 365. To preserve these parameters, you should employ a caching technique so they can be used after a completed OAuth flow completes. In the code below, you can see that the form data is cached in a cookie using the RedirectToIdentityProvider handler provided with Open ID Connect.
ConfigureAuth in Startup.Auth.cs
public void ConfigureAuth(IAppBuilder app){ ApplicationDbContext db = new ApplicationDbContext(); app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType); app.UseCookieAuthentication(new CookieAuthenticationOptions()); app.UseOpenIdConnectAuthentication( new OpenIdConnectAuthenticationOptions { ClientId = clientId, Authority = Authority, PostLogoutRedirectUri = postLogoutRedirectUri, TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters { // instead of using the default validation (validating against a single issuer value, as we do in line of business apps), // we inject our own multitenant validation logic ValidateIssuer = false, }, Notifications = new OpenIdConnectAuthenticationNotifications() { // If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away. AuthorizationCodeReceived = (context) => { var code = context.Code; ClientCredential credential = new ClientCredential(clientId, appKey); string signedInUserID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value; AuthenticationContext authContext = new AuthenticationContext(Authority, new ADALTokenCache(signedInUserID)); AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode( code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, graphResourceId); //cache the token in session state HttpContext.Current.Session[SettingsHelper.UserTokenCacheKey] = result; return Task.FromResult(0); }, RedirectToIdentityProvider = (context) => { FormDataCookie cookie = new FormDataCookie(SettingsHelper.SavedFormDataName); cookie.SaveRequestFormToCookie(); return Task.FromResult(0); } } });} |
Opening and Saving Files
One of the form parameters Office 365 posts to the application is the GET URI for the file. The code below shows the controller to Open the file using this URI. Notice that is first loads the form data using ActivationParameters object, then gets an access token, and retrieves the file. The controller view also adds a number of ViewData values that will be used later for saving changes to the file (including the refresh token, resource, and file put URI).
View Controller for Opening Files
public async Task<ActionResult> Index(){ //get activation parameters off the request ActivationParameters parameters = ActivationParameters.LoadActivationParameters(System.Web.HttpContext.Current); //try to get access token using refresh token var token = await GetAccessToken(parameters.ResourceId); //get the image HttpClient client = new HttpClient(); client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token.AccessToken); var imgBytes = await client.GetByteArrayAsync(parameters.FileGet); //return the image as a base64 string ViewData["img"] = "data:image/png;base64, " + Convert.ToBase64String(imgBytes); ViewData["resource"] = parameters.ResourceId; ViewData["refresh_token"] = token.RefreshToken; ViewData["file_put"] = parameters.FilePut; ViewData["return_url"] = parameters.FilePut.Substring(0, parameters.FilePut.IndexOf("_vti_bin")); return View();} |
Saving changes to the file is implemented in a Web API controller. The application POSTs the save details including the updated image, resource, refresh token, and file put URI to this end-point. The end-point gets an access token (using the refresh token) and POST and update to the existing image.
WebAPI Controller to Save Files
[Route("api/Save/")][HttpPost]public async Task<HttpResponseMessage> Post([FromBody]SaveModel value){ //get an access token using the refresh token posted in the request var token = await GetAccessToken(value.resource, value.token); HttpClient client = new HttpClient(); client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token.AccessToken); //convert base64 image string into byte[] and then stream byte[] bytes = Convert.FromBase64String(value.image); using (Stream stream = new MemoryStream(bytes)) { //prepare the content body var fileContent = new StreamContent(stream); fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); var response = await client.PostAsync(value.fileput, fileContent); } return Request.CreateResponse<bool>(HttpStatusCode.OK, true);} |
Client-side Script for Calling the Save Web API
var canvas = document.getElementById("canvas");var data = JSON.stringify({ token: $("#refresh_token").val(), image: canvas.toDataURL("image/png").substring(22), resource: $("#resource").val(), fileput: $("#file_put").val()});$.ajax({ type: "POST", contentType: "application/json", url: "../api/Save", headers: { "content-type": "application/json" }, data: data, dataType: "json", success: function (d) { toggleSpinner(false); window.location = $("#return_url").val(); }, error: function (e) { alert("Save failed"); }}); |
The completed solution uses a highly modified version of sketch.js to provide client-side drawing on a canvas element (modified to support images and scaling).
Conclusion
In my opinion, these new file handler add-ins are incredibly powerful. Imagine the scenarios for proprietary file extensions or extensions that haven’t traditionally been considered a first-class citizen in Office 365 (ex: .cad, .pdf, etc). You can download the .png file handler add-in solution from GitHub: https://github.com/OfficeDev/Image-FileHandler. You should also checkout Dorrene Brown's (@dorreneb) Ignite talk on File Handler Add-ins and Sonya Koptyev's (@SonyaKoptyev) Office Dev Show on the same subject.