And now for something completely different! While I normally talk about Dynamics 365 Portals, we’ve had a few requests recently around SharePoint and Dynamics 365 integration, specifically around uploading documents to SharePoint via a custom interface. I initially thought that it would be a topic well covered on the Dynamics blogosphere but, to my surprise, I couldn’t find a lot out there. So I spent some time figuring it out, and thought I’d share my findings.
We wanted to create a web resource that allowed a user to upload a document directly to SharePoint. While we are aware that an out-of-the-box SharePoint integration exists, we wanted a bit more control, including a custom file name and additional metadata.
Uploading a document in chunks, setting the file name, and setting meta are all things easily accomplished via the SharePoint REST API. So we felt confident that what we were trying to do was possible. The trick was getting the authentication working without requiring the user to login specifically into SharePoint. Assuming that both Dynamics and SharePoint are in the same Azure AD tenant, we figured this should be possible.
To call the SharePoint REST API, all we needed was an OAuth token with the correct privileges. Since we’re already logged into Dynamics with the same Azure AD user, how hard could it be to get that access token?
First, I think it’s important to provide a little background on OAuth implicit flow and Azure AD.
In our solution, we know that in order to get access to the web resource, the user must login to Dynamics 365 first. If an organization is using Azure AD, this process with require their main Azure AD credentials, and once provided, typically users won’t need to re-enter them to access other resources secured with Azure AD. Typically authentication is handled via cookies or browser storage, and for security reasons you can only access cookies or storage that were created by the current domain. So how can you log in to a URL that ends with crm.dynamics.com and also be signed into another site that ends with sharepoint.com?
This is achieved by having users log in on a shared page. If using Azure AD, whether a user is logging into Dynamics or SharePoint, the domain of the URL of the login page is login.microsoftonline.com. Once you enter valid credentials, you are redirected to the appropriate application; that redirect includes a token that the application uses to log you into that system specifically. If you were to visit a different application, you would be redirected to the login.microsoftonline.com domain momentarily, but if you’re already authenticated you won’t be asked for you credentials and will be redirected immediately to that other application, again with a token, and you’ll be logged in.
It is possible with implicit flow for the token that is returned to be the access token we need to call the SharePoint REST API, and it is possible to do this without the user even noticing; it is a non-trivial, but still fairly straight-forward task. Thankfully there are libraries (like ADAL.js) that can do this for you. At a high level, the process looks something like:
While we are by no means SharePoint experts, we use it internally and on a number of client projects, and have recently spent a fairly large amount of time becoming more familiar with the Azure AD implementation of OAuth. Like many things, once we figured it out, it didn’t turn out to be overly complicated, but we ventured down a few wrongs paths before finding the correct method.
Again, the main challenge was: how do I acquire the access token that we need to pass in the Authorization header to the SharePoint REST API?
The SharePoint documentation seems to be more concerned with how to acquire these tokens from within SharePoint itself, since the documentation seems to target the development of SharePoint Add-ins. This didn’t seem too helpful for us.
Our first attempt was to use the same ADAL library that we’ve been using recently for Dynamics 365 Portals Companion Apps. By starting from the PBAL.js library (which I believe is derived from this), we tried to determine the necessary parameters to pass to the login.microsoftonline.com domain to acquire the token. The main parameter we had trouble with was scope, which is essentially the permission that we were requesting. We figured that this would have something to do with requesting access to SharePoint, but couldn’t find any value here that would give us a valid token that would work with SharePoint.
Next, we considered using the new Microsft Graph API for SharePoint, but it was in beta so we weren’t comfortable using it in production.
After a bit more trial and error, I stumbled upon this post, which provided the information I needed: in the past we’ve been using v2 of the Azure AD OAuth service; SharePoint right now only works with the v1 service.
Now, this is not to be confused with the version of OAuth. Both v1 and v2 of the Azure AD OAuth Implementation use OAuth v2.
Once we figured that out, and we discovered that one of the main differences between the two is that in v1 you don’t specify scope (which was the parameter we couldn’t figure out), but instead you pass a resource parameter, and this resource is simply the URL of your SharePoint instance (https://orgname.sharepoint.com), we were in business.
We updated the getAuthorizeUri function to be:
var b2cAuthUri = "https://login.microsoftonline.com/" + params.tenant + "/oauth2/authorize?" + "client_id=" + params.clientId + "&response_type=token" + "&redirect_uri=" + encodeURIComponent(redirectUrl) + "&resource=" + encodeURIComponent(params.resource) + "&response_mode=fragment" + "&state=12345" + "&nonce=12345" + "&prompt=none" + "&domain_hint=organizations" + "&login_hint=" + params.oid;A few notes:
With that, we were able to get a token which we could use successfully to call the SharePoint REST API.
One final note: be careful about what URL you use for the redirect URL. Because the redirect URL is only used in the hidden iframe, the user doesn’t see it. The code also doesn’t really care what’s on that page – it’s only concerned with what’s in the query string parameters of the URL. But, if you choose poorly, things will not work.
But it’s also important that the page that you point it at doesn’t itself force a redirect. This is what we ran into when we used the root URL of our D365 as the redirect URL (for example, https://orgname.crm.dynamics.com/). This URL actually forces a redirect to https://orgname.crm.dynamics.com/main.aspx, and doesn’t maintain the query string parameters. So the code that fires when the iframe is loaded will see the page with main.aspx, and missing the query string parameter containing the all-important token. We had better results when we used the path of the web resource we were creating (for example, https://orgname.crm.dynamics.com/WebResources/new_/SharePoint.html).