Understanding in Depth – Cross Origin resource sharing (CORS) in Dynamics CRM 2016

First don’t get overwhelmed by the title of the topic. All this means how to connect to CRM from a standalone HTML Page using the web-api. Prior to CRM 2016, you cannot connect to CRM OData service from any HTML Page outside dynamics CRM. However with CRM 2016 you are able to do this.

If you search by CORS in SDK, you would find entire walkthrough of how to do this. I am going to show the same example here. So why a repeat of already what’s there in SDK.

Well if you are expert in OAuth 2.0 and know how it works you can perhaps easily map the concept to the steps provided in the SDK. However if you are not so sure about OAuth 2.0 you may find extremely difficult to understand what’s going on behind the scenes. And believe me, some experienced Dynamics CRM consultants have walked up to my desk and asked for help to figure this stuff out. So penning it down so that other’s might find it useful too.

First I will give a brief introduction of how OAuth 2.0 works and then will draw an analogy of how the same works in Dynamics CRM. So let’s start with OAuth 2.0 first.

To explain OAuth let me use facebook example. Delighted? Well what more common example can I provide other than something related to ubiquitous facebook.

Frequently you access apps from facebook which prompts you that it would get your information from Facebook and then once you accept it, you have access to the app. If you are not aware, this is done using OAuth 2.0. Well lets’ view it in diagrammatic sense.

image

Pic courtesy (http://tutorials.jenkov.com/oauth2/overview.html)

So let’s summarize the steps with the help of some website which authenticates via Facebook

  • User tries to access the Website (client in the above diagram)
  • In the login screen, user clicks on ‘Login via Facebook’ which is a common scenario these days.
  • The website redirects the user to the Facebook authorization page, where you enter the facebook credentials.
  • The authorization server of Facebook redirects the user to the website with the authorization code
  • The website now sends another request with the Authorization Code + Client_Id + Client_secret. The client_id and the client_secret and generated when the website has registered itself with facebook. To explain in simple terms, client_id is like some unique identifier for the client and the client_secret is some secret shared between only facebook and the client )which is this case is the website).
  • The authorization server of Facebook validates the values and generates an access token.
  • From the next time onwards, the client just sends the access token in all its requests to act on behalf of the user.

So if we take the above scenario, we come up with four actors in OAuth 2.0

  • Client – The website which uses facebook for authentication.
  • Resource Owner – The end-user who is accessing the website
  • Resource server – Facebook
  • Authorization Server – The authorization server that Facebook uses for identity management. Sometimes the resource server and the authorization server might be same.

Now our requirement here is to access CRM data from a standalone HTML Page. Let’s map this requirement to the above example and see who the actors here are.

  • Client – Standalone HTML web Page
  • Resource Owner – End user accessing the HTML page through browser
  • Resource Server – Dynamics CRM Online Instance
  • Authorization Server – Azure Active Directory

Things getting clear? Delighted to go ahead? I am glad if you are feeling so.  Well let’s get started then. There is nothing fancy I will do in the code. I will just copy and paste the code from SDK. But I will explain each step as to why I am doing rather than what I am doing.

Below is the code for HTML file. Create a HTML file in Visual Studio. Name it ImplicitAuthorizationCRM.htm. And copy and paste the below code in the html file.Keep the file open in visual studio. We will come back to this soon.

<!DOCTYPE html>
<html>
<head>
 <title>Simple SPA</title>
 <meta charset="utf-8" />
 http://adal.js
 "use strict";
 //Set these variables to match your environment
 var organizationURI = "https://xrmtr14.crm.dynamics.com"; //The URL to connect to CRM Online
 var tenant = "xrmtr14.onmicrosoft.com"; //The name of the Azure AD organization you use
 var clientId = "b140fe85-6410-40ba-9aee-cfc37f630949"; //The ClientId you got when you registered the application
 var pageUrl = "http://localhost:64754/ImplicitAuthorizationCRM.html"; //The URL of this page in your development environment when debugging.
 var user, authContext, message, errorMessage, loginButton, logoutButton, getAccountsButton, accountsTable, accountsTableBody;
 //Configuration data for AuthenticationContext
 var endpoints = {
 orgUri: organizationURI
 };
 window.config = {
 tenant: tenant,
 clientId: clientId,
 postLogoutRedirectUri: pageUrl,
 endpoints: endpoints,
 cacheLocation: 'localStorage', // enable this for IE, as sessionStorage does not work for localhost.
 };
 document.onreadystatechange = function () {
 if (document.readyState == "complete") {
 //Set DOM elements referenced by scripts
 message = document.getElementById("message");
 errorMessage = document.getElementById("errorMessage");
 loginButton = document.getElementById("login");
 logoutButton = document.getElementById("logout");
 getAccountsButton = document.getElementById("getAccounts");
 accountsTable = document.getElementById("accountsTable");
 accountsTableBody = document.getElementById("accountsTableBody");
 //Event handlers on DOM elements
 loginButton.addEventListener("click", login);
 logoutButton.addEventListener("click", logout);
 getAccountsButton.addEventListener("click", getAccounts);
 //call authentication function
 authenticate();
 if (user) {
 loginButton.style.display = "none";
 logoutButton.style.display = "block";
 getAccountsButton.style.display = "block";
 var helloMessage = document.createElement("p");
 helloMessage.textContent = "Hello " + user.profile.name;
 message.appendChild(helloMessage)
 }
 else {
 loginButton.style.display = "block";
 logoutButton.style.display = "none";
 getAccountsButton.style.display = "none";
 }
 }
 }
 // Function that manages authentication
 function authenticate() {
 //OAuth context
 authContext = new AuthenticationContext(config);
 // Check For & Handle Redirect From AAD After Login
 var isCallback = authContext.isCallback(window.location.hash);
 if (isCallback) {
 authContext.handleWindowCallback();
 }
 var loginError = authContext.getLoginError();
 if (isCallback && !loginError) {
 window.location = authContext._getItem(authContext.CONSTANTS.STORAGE.LOGIN_REQUEST);
 }
 else {
 errorMessage.textContent = loginError;
 }
 user = authContext.getCachedUser();
 }
 //function that logs in the user
 function login() {
 authContext.login();
 }
 //function that logs out the user
 function logout() {
 authContext.logOut();
 accountsTable.style.display = "none";
 accountsTableBody.innerHTML = "";
 }
 //function that initiates retrieval of accounts
 function getAccounts() {
 getAccountsButton.disabled = true;
 var retrievingAccountsMessage = document.createElement("p");
 retrievingAccountsMessage.textContent = "Retrieving 10 accounts from " + organizationURI + "/api/data/v8.0/accounts";
 message.appendChild(retrievingAccountsMessage)
 // Function to perform operation is passed as a parameter to the aquireToken method
 authContext.acquireToken(organizationURI, retrieveAccounts)
 }
 //Function that actually retrieves the accounts
 function retrieveAccounts(error, token) {
 // Handle ADAL Errors.
 if (error || !token) {
 errorMessage.textContent = 'ADAL error occurred: ' + error;
 return;
 }
 var req = new XMLHttpRequest()
 req.open("GET", encodeURI(organizationURI + "/api/data/v8.0/accounts?$select=name,address1_city&$top=10"), true);
 //Set Bearer token
 req.setRequestHeader("Authorization", "Bearer " + token);
 req.setRequestHeader("Accept", "application/json");
 req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
 req.setRequestHeader("OData-MaxVersion", "4.0");
 req.setRequestHeader("OData-Version", "4.0");
 req.onreadystatechange = function () {
 if (this.readyState == 4 /* complete */) {
 req.onreadystatechange = null;
 if (this.status == 200) {
 var accounts = JSON.parse(this.response).value;
 renderAccounts(accounts);
 }
 else {
 var error = JSON.parse(this.response).error;
 console.log(error.message);
 errorMessage.textContent = error.message;
 }
 }
 };
 req.send();
 }
 //Function that writes account data to the accountsTable
 function renderAccounts(accounts) {
 accounts.forEach(function (account) {
 var name = account.name;
 var city = account.address1_city;
 var nameCell = document.createElement("td");
 nameCell.textContent = name;
 var cityCell = document.createElement("td");
 cityCell.textContent = city;
 var row = document.createElement("tr");
 row.appendChild(nameCell);
 row.appendChild(cityCell);
 accountsTableBody.appendChild(row);
 });
 accountsTable.style.display = "block";
 }
 <style>
 body {
 font-family: 'Segoe UI';
 }
 table {
 border-collapse: collapse;
 }
 td, th {
 border: 1px solid black;
 }
 #errorMessage {
 color: red;
 }
 #message {
 color: green;
 }
 </style>
</head>
<body>
 <button id="login">Login</button>
 <button id="logout" style="display:none;">Logout</button>
 <button id="getAccounts" style="display:none;">Get Accounts</button>
<table id="accountsTable" style="display:none;"> <thead><tr><th>Name</th><th>City</th></tr></thead> <tbody id="accountsTableBody"></tbody> </table> </body> </html>

Let us complete each of the steps for this requirement. I assume you have already a trial or a working environment of Dynamics CRM Online 2016. If not please create a trial org and then come back to this step.

Configure Azure AD:

This step adds the Dynamics CRM Active Directory to your azure subscription. It’s very logical right? After all to access Dynamics CRM, you need to validate the users using Dynamics CRM AD.

If you do not have any existing azure subscription, go to https://azure.microsoft.com/en-in/pricing/free-trial/. Create a free subscription and then navigate to https://manage.windowsazure.com

  • Click on Active Directory –> New (at the bottom of the screen) –> Directory –> Custom create. You should get a pop like the screenshot as shown below.
image
  • Select I am ready to be signed out and then proceed.
image
  • You would be asked to login where you enter the CRM organization credentials and confirm. Once you authenticate you are done and then in sign in back to the azure account and go to the Active Directory section again. You should be able to see the CRM active directory added.
  • Open the new added AD. You should see a screen like the one below.
image
  • Click on Domains in the top bar and note it down somewhere. You will need it soon. This is you Authorization server tenant. In my case it is xrmtr14.onmicrosoft.com.
image
  • Remember we need the client_id and the client_secret to get the access token as I explained above. So for that we need to register the HTML web Application in our Azure AD. Let’s do that.

Register Application with Azure

  • Click on Applications –> Add. Select the option highlighted below in the pop-up.
image
  • Enter a name. I enter ‘AzureAuthWebApp’ and select Web Application since our’s is a simple HTML page running in browser. Proceed to the next section.
  • image
  • You will be asked to enter the sign-on url and the App ID URI. So what are these? Sign-On url is typically the url of your page where you ask your users to sign in. Since we have only one page, to get the sign-on url, right click the HTML file opened in visual studio and then select ‘View in Browser’. Copy the URL and paste in the box. For me it is coming – http://localhost:64754/ImplicitAuthorizationCRM.html. For you the port number might be different.
  • App ID URI – remember anything you deploy in Azure, everything is a service which must be identified by a uri. This represents the Unique URI identifier for your application. I entered https://xrmtr14.onmicrosoft.com/AzureHtmlAuthentication. Typically this should be a valid URL and unique as well. I use the CRM Domain in the URL since I know using that will make it unique as there is no-one else who can use that domain.
image
  • The application is now added.
  • Now go to the application and click on configure.
image
  • Scroll down and you would get the client id.
image
  • Copy that and keep it. We will need that very soon.

Provide Web Application (client) access to Dynamics CRM (resource server)

  • Now what? Well this application will connect to your CRM organization and try to access the data right? For that we need to give permission to this application to access Dynamics CRM instance. Scroll down further and you would see the button add application. Click on that. You would get a screen like the one shown below. Click on Dynamics CRM Online
image
  • After the application is added, click on Delegated permissions and select – Access CRM Online Organization as Users. Remember to click save at the bottom of the screen.
image

So through the above steps what you have basically done is you have registered our Web App (client) with the Authorization Server (azure AD) to access resources from the resource server (Dynamics CRM Online). Are we done here? Am I missing something? Yes. Let me explain.

You remember, apart from the client id, I also told about the client secret which is some secret code in which the Application and the resource server talks. Then why I did not show you how to generate secret code here. Well in our case, the client is just some simple scripts implemented in browser. Passing the secret code through the browser is not secure as you can easily debug to find out the secret code.

In cases like this OAuth 2.0 uses the implicit flow.

The implicit grant is a simplified authorization code flow optimized for clients implemented in a browser using a scripting language such as JavaScript. In the implicit flow, instead of issuing the client an authorization code, the client is issued an access token directly (as the result of the resource owner authorization). The grant type is implicit, as no intermediate credentials (such as an authorization code) are issued (and later used to obtain an access token). For more details use this link http://tools.ietf.org/html/rfc6749

By default, implicit flow is not allowed in Azure AD. To enable implicit flow, you have to do the following.

Click on Manage Manifest. And then click on Download. A .json file would be downloaded. Using the manifest file, you can edit the properties of the application which are not editable through the azure management portal.

image

Open the json file. You would find a property called “oauth2AllowImplicitFlow”. Change the value from false to true and save the file. Upload the file back clicking on Manage Manifest again and this time selecting the upload option.

So now you are all set.

With the HTML that you opened previously, check for the properties highlighted in the screenshot. If you have followed all the steps above, you should be able to replace the properties with appropriate values. I leave that to you. For pageUrl you might use the same value as the Sign-on url for the application that you put on the azure.

image

To do some heavy query forming stuff and all, Microsoft provides you Active Directory authentication Library (adal.js) which I have used in this. You can download the same from this URl – https://secure.aadcdn.microsoftonline-p.com/lib/1.0.0/js/adal.min.js.

You may need to update the reference to the file in the HTML Page depending on where you have kept the adal.js. I have placed it in the same folder as my html page. If the reference in the script tag is not correct, this example will not work.

So all set. Hit the sign-on url in chrome or mozilla. Wondering why I did not tell IE. Well with IE you will be able to authenticate but once you try to retrieve the accounts you would get an error – “User account identifier is not provided”. This is because Adal.js uses iframes to get CORS API tokens for resources other than the SPA’s own backend. Iframe request needs to access the browser’s cookies to authenticate with AAD and get the access token. Cookies are not accessible when you run in localhost from IE. You need fully qualified domain names to access cookies from iframe in IE.

Click on the login button. You would be redirected to login.microsoftonline.com to authorize. Copy the url.  You would be able to see the client_id is being passed along with the url of our page. ADAL.js does the heavy work of forming the url for you.

Enter you CRM credentials and once you authenticate you would be redirected to your application. Click on Get Accounts and you get the list of accounts.

 

image

Curious to know how the access token looks. Well in the HTML file just put a debugger in the retrieveAccounts method and once you click on Get Accounts, you can see the access token in your debugger.

Hope this helps!

11 thoughts on “Understanding in Depth – Cross Origin resource sharing (CORS) in Dynamics CRM 2016”

  1. Very good post Debajit, just followed step by step, no issues, it’s great.
    Only one thing was missing in the html provided, below two controls, but it is minor.
    As you said, it was breaking in IE 🙂

  2. Hi Debajit,
    Thanks for response 🙂 🙂
    Basically I want to connect from an external WEB API from Dynamics CRM.
    Please share any example or article. Please

    1. Hi Pankaj,
      There are points that you should consider.
      1. Is your WEBAPI exposed over the internet or deployed in your server behind firewall?
      2. What authentication parameters does your WEb API expect. I assume it would expect a JWT token to be passed in the header. If that is the case you would need to form the JWT token which is understood by your Web API and then send the JWT in the request header from Dynamics 365

  3. Hi Dabajit,
    1.My WEB API neither exposed over the internet nor deployed on my server. I am just using WEB API project in Visual Studio.
    2.My WEB API is using Bearer Authentication.

  4. Hi Debajit,
    Can i use this concept with on premise CRM 365.
    We have to allow external user, who are not in CRM, to login to the web application which will eventually retrieve data from CRM.

    1. Hi Mohammed,
      Pretty late reply. And i hope you have found the solution by now. Been travelling mostly and hence out of emails.
      Your scenario is completely different from what is described in the post. Your scenario demand building a custom portal for external users to see the CRM data. Now you can build your own portal or for some quick stuff you can go to the highly recommended ADX portals or peak portals for this solution.
      -Debajit

Comments are closed.