次の方法で共有


Connecting to Office 365 from an Office Add-in

Earlier in the year, I authored a post on Connecting to SharePoint from an Office add-in. In that post, I illustrated 5 approaches that were largely specific to SharePoint. However, the last pattern connected to SharePoint using the Office 365 APIs. SharePoint is one of many powerful services exposed through the Office 365 APIs. In this post, I’ll expand on leveraging the Office 365 APIs from an Office add-in. I’ll try to clear up some confusion on implementation and outline some patterns to deliver the best user experience possible with Office add-in that connect to Office 365. Although I’m authoring this post specific to the Office 365 APIs, the same challenges exist for almost any OAuth scenario with Office add-ins (and the same patterns apply).

Mail CRM sample provided in post

 

The Office add-in Identity Crisis

Since 2013, users have become accustomed to signing into Office. Identity was introduced into Office for license management and roaming settings such as file storage in OneDrive and OneDrive for Business. However, this identity is not currently made available to Office add-ins. The one exception is in Outlook mail add-ins, which can get identity and access tokens specifically for calling into Exchange Online APIs. All other scenarios (at the time of this post) require manual authentication flows to establish identity and retrieve tokens for calling APIs.

User may sign into Office, but identity isn't available to add-ins

 

Why Pop-ups are a Necessary Evil

Office add-ins can display almost any page that can be displayed in a frame and whose domain is registered in the add-in manifest (in the AppDomains section). Both of these constraints can be challenging when performing OAuth flows. Due to the popularity of clickjacking on the internet, it is common to prevent login pages from being display inside frames. The X-FRAME-Options meta tag in HTML makes it easy for providers to implement this safeguard on a widespread or domain/origin-specific basis. Pages that are not “frameable” will not load consistently in an Office add-in. For example, Office Online displays Office add-ins in IFRAME elements. Below is an example of an Office add-in displaying a page that cannot be displayed in a frame:

Office add-in display an page that is NOT "frameable"

 

The other challenge facing Office add-ins that perform OAuth flows is in establishing trusted domains. If an add-in tries to load any domain not registered in the add-in manifest, Office will launch the page in a new browser window. In some cases, this can be avoided by registering the 3rd party domain(s) in the AppDomains section of the add-in manifest (ex: https://login.microsoftonline.com). However, this might be impossible with identity providers that support federated logins. Take Office 365 as an example. Most large organizations use a federated login to Office 365 (usually with Active Directory Federation Services (ADFS)). In these scenarios, the organization/subscriber owns the federated login and the domain that hosts it. It is impossible for an add-in developer to anticipate all domains customers might leverage. Furthermore, Office add-ins do not support wildcard entries for trusted domains. In short, popups are unavoidable.

Rather than trying to avoid popup, it is better to accept them as a necessary evil in Office add-ins that perform OAuth/logins. Redirect your attention to popup patterns that can deliver a better user experience (which I cover in the next section).

Good User Experience without Identity

To address the challenges with identity in Office add-ins, I’m going to concentrate on patterns for improving the user experience in the popup and with “single sign-on”. For popups, we want to deliver an experience where the popup feels connected to the add-in (a feat that can be challenging in some browsers). For “single sign-on” we want to provide a connected experience without requiring the user to sign-in every time they use the add-in. Technically, this isn’t really “single sign-in” as much as token cache management (which is why I put "single sign-on" in quotes).

Mastering the Popup

Almost as soon as the internet introduced popup, they started being used maliciously by both hackers and advertisers. For this reason, popups have established a bad reputation and browsers have built-in mechanisms to control them.  These safeguards can make client-side communication between add-ins and popups problematic (don't get me started on IE Security Zones). Ultimately, we are using popups to acquire access tokens so that add-ins can make API calls. Instead of passing tokens back client-side (via window.opener or window.returnValue), consider a server-side approach that browsers cannot (easily) interfere with.

One server-side method for popup/add-in communication is by temporarily caching tokens on a server or in a database that both the popup and add-in can communicate with. With this approach, the add-in launches the popup with an identifier it can use to later retrieve the access token for making API calls. The popup performs the OAuth flow and then caches the token by the identifier passed from the add-in. This was the approach I outlined in the Connecting to SharePoint from an Office add-in blog post. It is solid, but relies upon cache/storage and requires the add-in to poll for tokens or the user to query for tokens once the OAuth flow is complete.

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

 

We can address both these limitations by delivering popup/add-in communication via web sockets. This method is similar to the previous approach. The add-in still passes an identifier to the popup window, but now “listens” for tokens using web sockets. The popup still handles the OAuth flow, but can now push the token directly to the add-in via the web socket the add-in is listening on (this “push” goes through a server and is thus considered server-side). The benefit of this method is that nothing needs to be persisted and the add-in can immediately proceed when it gets the access token (read: no polling or user actions required). Web sockets can be implemented numerous ways, but I’ve become a big fan of ASP.NET SignalR. Interestingly, SignalR already provides an identifier when a client established a connection to the server (which I can use as my identifier sent to the popup).

Sound complicated? It can be, so I’ll try to break it down. When the add-in launches, we need to get the identifier (to pass into the popup) and then start listening for tokens:

Get the Client Identifier and Start "listening" on Hub for Tokens

//initialize called when add-in loads to setup web socketsstateSvc.initialize = function () {    //get a handle to the oAuthHub on the server    hub = $.connection.oAuthHub;     //create a function that the hub can call to broadcast oauth completion messages    hub.client.oAuthComplete = function (user) {        //the server just sent the add-in a token        stateSvc.idToken.user = user;        $rootScope.$broadcast("oAuthComplete", "/lookup");    };     //start listening on the hub for tokens    $.connection.hub.start().done(function () {        hub.server.initialize();         //get the client identifier the popup will use to talk back        stateSvc.clientId = $.connection.hub.id;    });};

 

The client identifier is passed as part of the redirect_uri parameter in the OAuth flow of the popup:

Page loaded in popup for perform OAuth flow

https://login.microsoftonline.com/common/oauth2/authorize?client_id=cb88b4df-db4b-4cbe-be95-b40f76dccb14&resource=https://graph.microsoft.com/&response_type=code&redirect_uri=https://localhost:44321/OAuth/AuthCode/A5ED5F48-8014-4E6C-95D4-AA7972D95EC9/C7D6F7C7-4EBE-4F45-9CE2-EEA1D5C08372 //the User ID in DocumentDB //the Client Identifier listening on web socket for tokens...think of this as the "address" of the add-in

 

The OAuthController completes the OAuth flow and then uses the client identifier to push the token information to the add-in via the web socket:

OAuthController that handles the OAuth reply

[Route("OAuth/AuthCode/{userid}/{signalrRef}/")]public async Task<ActionResult> AuthCode(string userid, string signalrRef){    //Request should have a code from AAD and an id that represents the user in the data store    if (Request["code"] == null)        return RedirectToAction("Error", "Home", new { error = "Authorization code not passed from the authentication flow" });    else if (String.IsNullOrEmpty(userid))        return RedirectToAction("Error", "Home", new { error = "User reference code not passed from the authentication flow" });     //get access token using the authorization code    var token = await TokenHelper.GetAccessTokenWithCode(userid.ToLower(), signalrRef, Request["code"], SettingsHelper.O365UnifiedAPIResourceId);     //get the user from the datastore in DocumentDB    var idString = userid.ToLower();    var user = DocumentDBRepository<UserModel>.GetItem("Users", i => i.id == idString);    if (user == null)        return RedirectToAction("Error", "Home", new { error = "User placeholder does not exist" });     //update the user with the refresh token and other details we just acquired    user.refresh_token = token.refresh_token;    await DocumentDBRepository<UserModel>.UpdateItemAsync("Users", idString, user);     //notify the client through the hub    var hubContext = GlobalHost.ConnectionManager.GetHubContext<OAuthHub>();    hubContext.Clients.Client(signalrRef).oAuthComplete(user);     //return view successfully    return View();}

 

Here is a video that illustrates the web socket approach. Notice that the add-in continues on after the OAuth flow without the user having to do anything.

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

 

Cache Management

Ok, we have established a consistent and smooth method for getting tokens. However, you probably don’t want to force the user through this flow every time they use the add-in. Fortunately, we can cache user tokens to provide long-term access to Office 365 data. An access token from Azure AD only has a one hour lifetime. So instead, we will cache the refresh token, which has a sliding 14-day lifetime (maximum of 90 days without forcing a login). Caching techniques will depend on the type of app.

The Exchange/Outlook Team already has a published best practice for caching tokens in an Outlook mail add-in. It involves using the Identity Token that is available through JSOM (Office.context.mailbox.getUserIdentityTokenAsync) and creating a hashed combination of ExchangeID and AuthenticatedMetadataUrl. This hashed value is the lookup identifier the refresh token is stored by. The Outlook/Exchange Team has this documented on MSDN, including a full code sample. I followed this guidance in my solutions. For the sample referenced in this post, I used Azure’s DocumentDB (a NoSQL solution similar to Mongo) to cache refresh tokens by this hash value. Below, you can see a JSON document that reflects a cached user record. Take note of the values for hash and refresh_token:

DocumentDB record for user (with cached refresh token by hash)

 

For document-centric add-ins with Excel, Word, and PowerPoint, there is no concept of an identity in JSOM. Thus, these types of add-ins can’t take the same token caching approach as an Outlook mail add-in. Instead, we must revert to traditional web caching techniques such as cookies, session state, or database storage. I would probably not recommend local cache of the actual refresh tokens. So if you want to use cookies, try storing some lookup value in the cookie that the add-in can use to retrieve the refresh token stored on a server. Consider also that cookie caching in an Office add-in could expose information in a shared workstation scenario. Ultimately, be careful with your approach here.

Conclusion

I have full confidence that these add-in identity challenges will be short lived. In the meantime, the patterns outlined in this post can help deliver a better user experience to users. To get you jumpstarted, you can download a Mail CRM sample to uses these patterns and many more. You can also download the Office 365 API sample from the Connecting to SharePoint from an Office add-in post back in March. Happy coding!

Mail CRM Sample outlined in blog post: https://github.com/OfficeDev/PnP-Store/tree/master/DXDemos.Office365

Connecting with SharePoint from add-in sample (from March 2015): https://1drv.ms/1HaiupJ

Comments

  • Anonymous
    August 16, 2015
    Awesome blog post! I noticed that in your demo the user does not have to enter their credentials, I guess because the add-in is running from a browser. But, this will probably not work in the desktop client? E.g. in a Word Add-in  the Add-in runs in an in-private window/frame, so the user would have to log in. Is there any way we could provide SSO in a Word/Excel add-in with Office 365? Or do we have to wait for improvements in Office 2016?

  • Anonymous
    September 16, 2015
    Very interesting, I have struggled a lot for implementing the OAUTH flow in the addins sandboxed environment. So the popup solution is the recommended way to implement the OAUTH flow? I am not 100% convinced because this won't work in some environments such as mobile (even windows phone with IE) where popups are not supported. See my question on the msdn forum. social.msdn.microsoft.com/.../what-is-the-recommended-solution-for-implementing-the-oauth-20-flow-in-sandboxed-app-for-office benoitpatra.com/.../implementing-the-oauth-2-0-flow-in-app-for-office-sandboxed-environment

  • Anonymous
    December 16, 2015
    Is this still a valid approach after all the new things announced and launched? (Microsoft Graph API, etc.)?

  • Anonymous
    December 16, 2015
    That is indeed a very important post. The approach you describe here is much more robust than the one I presented with window.opener etc. I managed to implement what you suggested, this is definitely the best know solution for this problem. Moreover it works for a code authorization flow which could not be addressed with the approach mentioned before. @Sonny I do believe this is the approach to be taken. I am using it for a multitenant app with authorization flow and Azure Graph API (among others)

  • Anonymous
    December 17, 2015
    Thanks for letting us know how it went well for you @Benoit P @Roland, any new things or updates to share relative to this post?

  • Anonymous
    December 17, 2015
    The comment has been removed