D365 Authentication – connect my apps

With the constant push to make data more secure D365 now includes Application Users that can be configured to allow access to D365 from external applications. There are numerous articles around explaining how to create your Application User in D365 using Azure AD App Registrations – this one from PowerObjects has a good explanation of steps required to achieve this.

In this post I wanted to briefly describe the next steps for using that authentication method in your app to perform CRUD operations. To demonstrate this I created a simple Console App to connect to a D365 instance and perform some simple operations using both the OrganizationWebProxyClient and the CrmServiceClient. For reference, I added a package from NuGet called Microsoft.CrmSdk.XrmTooling.CoreAssembly which contains the official Microsoft.Xrm.Tooling.Connector assembly. The same NuGet package also contains the Microsoft.CrmSdk.CoreAssemblies dependency which contains the Microsoft.Xrm.Sdk.dll. Within the console app I created a class that manages to authentication.

using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System;
using System.Collections.Generic;

namespace OrgService.Connect
{
    class AuthHook : Microsoft.Xrm.Tooling.Connector.IOverrideAuthHookWrapper
    {
        // In memory cache of access tokens
        Dictionary<string, AuthenticationResult> accessTokens = new Dictionary<string, Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationResult>();

        public void AddAccessToken(Uri orgUri, Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationResult accessToken)
        {
            // Access tokens can be matched on the hostname,
            // different endpoints in the same organization can use the same access token
            accessTokens[orgUri.Host] = accessToken;
        }

        public string GetAuthToken(Uri connectedUri)
        {
            // Check if you have an access token for this host
            if (accessTokens.ContainsKey(connectedUri.Host) && accessTokens[connectedUri.Host].ExpiresOn > DateTime.Now)
            {
                return accessTokens[connectedUri.Host].AccessToken;
            }
            else
            {
                accessTokens[connectedUri.Host] = GetAccessTokenFromAzureAD(connectedUri);
            }
            return accessTokens[connectedUri.Host].AccessToken;
        }

        private AuthenticationResult GetAccessTokenFromAzureAD(Uri orgUrl)
        {
            string organizationUrl = "https://myD365instance.crm6.dynamics.com";
            string clientId = "00000000-0000-0000-0000-000000000001";    // This is the Application ID from your App Registration
            string appKey = "<the client secret for your app>";          // The Client Secret from your App Registration
            string aadInstance = "https://login.microsoftonline.com/";
            string tenantID = "00000000-0000-0000-0000-000000000001";    // The GUID of your Azure Tenant ID. See the article above for details on finding this value.

            ClientCredential clientcred = new ClientCredential(clientId, appKey);
            AuthenticationContext authenticationContext = new AuthenticationContext(aadInstance + tenantID);
            AuthenticationResult authenticationResult = authenticationContext.AcquireToken(organizationUrl, clientcred);

            accessTokens[new Uri(organizationUrl).Host] = authenticationResult;

            return authenticationResult;
        }
    }
}

To access the D365 instance using the OrganizationWebProxyClient we can use the following code

                string organizationUrl = "https://myD365instance.crm6.dynamics.com";

                var hook = new AuthHook();

                var requestedToken = hook.GetAuthToken(new Uri(organizationUrl));

                using (var webProxyClient = new OrganizationWebProxyClient(new Uri($"{organizationUrl}/XRMServices/2011/Organization.svc/web"), false))
                {
                    webProxyClient.HeaderToken = requestedToken;

                    // Test with a basic WhoAmI request first
                    OrganizationRequest request1 = new OrganizationRequest()
                    {
                        RequestName = "WhoAmI"
                    };
                    OrganizationResponse response1 = webProxyClient.Execute(request1);

                    // We are also able to create an instance of the OrganizationService and run queries against it
                    IOrganizationService organizationService = webProxyClient as IOrganizationService;

                    Entity entity = organizationService.Retrieve("account", new Guid("92348762-0D32-E611-80EC-B38A27891203"), new Microsoft.Xrm.Sdk.Query.ColumnSet("name", "preferredcontactmethodcode"));

                }

Using the Microsoft.Xrm.Tooling.Connector we require a slightly different approach when using the authenticated Application User. It still uses the AuthHook class but through the AuthOverrideHook property. This uses the same AuthHook object we created earlier for the OrganizationWebProxyClient.

                // Register the hook with the CrmServiceClient
                CrmServiceClient.AuthOverrideHook = hook;

                // Create a new instance of CrmServiceClient, pass your organization url and make sure useUniqueInstance = true!
                var client = new CrmServiceClient(new Uri(organizationUrl), useUniqueInstance: true);

                // Test with a basic WhoAmI request first
                OrganizationRequest request2 = new OrganizationRequest()
                {
                    RequestName = "WhoAmI"
                };
                OrganizationResponse response2 = client.Execute(request2);

                Entity entity1 = client.Retrieve("account", new Guid("92348762-0D32-E611-80EC-B38A27891203"), new Microsoft.Xrm.Sdk.Query.ColumnSet("name", "preferredcontactmethodcode"));

Advertisements

D365 – Web API – Custom Actions & Entity Reference Inputs

Using v9.0 of the D365 Web Api we are able to trigger Actions. Using a standard URL structure we can call those Actions from client-side script, for example – POST /api/data/v9.0/incidents(00000000-0000-0000-0000-000000000000)/Microsoft.Dynamics.CRM.new_CustomIncidentAction.

When these Actions have Input parameters they can be added to the body of the request but knowing how to structure the body can be a bit of a challenge. For numeric, character and boolean values it’s just a case of constructing the relevant JSON object. For EntityReference’s there is a specific structure which will require you to find some details in the ODataV4Metadata.xml file. This file can be downloaded by going to your D365 instance under Settings | Customizations | Developer Resources and clicking on the Download OData Metadata link under the Instance Web API section. Search the file for the name of your Action you want to call and you should get something similar to this…

<Action Name="new_CustomIncidentAction" IsBound="true">
<Parameter Name="entity" Type="mscrm.incident" Nullable="false" />
<Parameter Name="TeamId" Type="mscrm.team" Nullable="false" />
</Action>

The first is the GUID of the incident we are running the Action against. The second is the one we are interested in as this is our input EntityReference.

To construct the JSON body of our request we need three pieces of information

  1. Parameter name – this we can get from the XML (above). The name is case sensitive.
  2. The OData type – this is also specified in the XML, but needs to be defined in a slightly different format than is defined in the XML.
  3. The EntityReference Id – the GUID value for the entity

The JSON object for our EntityReference input parameter can be specified like this.

{ "TeamId": {
	"@odata.type": "Microsoft.Dynamics.CRM.team",
	"teamid": "2ab7a6d5-9a36-e611-80e7-c4346bc48ef4" 
	} 
}

D365 JavaScript Web Resource Library Usage

It’s fairly straight forward to find which Forms use a Web Resource with the ‘Show Dependencies’ option, but that will list Forms where the JavaScript library has been added to the form in the Form Libraries. This doesn’t give you any indication whether the functions in the JavaScript file have actually been used in the form.

Obviously, you can scroll through each Tab and Field in the Event Handlers to look for occurrences of an event, but if you are doing maintenance on a form you didn’t originally develop then it could take a while to scroll though all the possible events. The Event Libraries and Event Handlers are stored in the formjson column of the systemform entity so with a simple FetchXML query you can get a list of Forms that actually use functions from a particular library. The example below will return all Forms where the msdyn_/Utils/head.js Web Resource has been used.The key part is the Filter Condition that is looking for a LibraryName that matches our JavaScript library. The formjson content contains an array of EventHandlers that define the EventName, FunctionName and LibraryName.

<fetch>
  <entity name='systemform' >
    <attribute name='name' />
    <attribute name='formjson' />
    <attribute name='formactivationstate' />
    <attribute name='type' />
    <attribute name='objecttypecode' />
    <filter>
      <condition attribute='formjson' operator='like' 
         value='%&quot;LibraryName&quot;:&quot;msdyn_/Utils/head.js%' />
    </filter>
  </entity>
</fetch>

The next step is to figure out which EventHandlers, if any, use that library in those forms. To do that I created a simple LINQPad script to return a list of the functions in each form.

void Main()
{
	string url = "https://myorgname.crm6.dynamics.com"; 
	string username = "my.user@myorgname.onmicrosoft.com";
	string password = Util.GetPassword("d365-admin");
	
	CrmServiceClient conn = new CrmServiceClient($"Url={url};Username={username};Password={password}; AuthType=Office365");

	conn.OrganizationServiceProxy.Timeout = new System.TimeSpan(0, 3, 0);
	
	conn.OrganizationServiceProxy.EnableProxyTypes();
	
	IOrganizationService orgService = conn.OrganizationWebProxyClient != null ? (IOrganizationService)conn.OrganizationWebProxyClient : (IOrganizationService)conn.OrganizationServiceProxy;

	string pagingCookie = null;
	
	EntityCollection formsCol = null;
	
	List<Entity> forms = new List<Microsoft.Xrm.Sdk.Entity>();
	
	do
	{
		string fetchXML = $@"<fetch>
		  <entity name='systemform' >
		    <attribute name='name' />
		    <attribute name='formjson' />
		    <attribute name='formactivationstate' />
		    <attribute name='type' />
		    <attribute name='objecttypecode' />
		    <filter>
		      <condition attribute='formjson' operator='like' value='%&quot;LibraryName&quot;:&quot;msdyn_/Utils/head.js%' />
		    </filter>
		  </entity>
		</fetch>";

		formsCol = orgService.RetrieveMultiple(new FetchExpression(fetchXML));
		
		if (formsCol.Entities.Count > 0)
		{
			forms.AddRange(formsCol.Entities);	
		}
		pagingCookie = formsCol.PagingCookie;
	}
	while (formsCol.MoreRecords);

	forms.Select(e => new
	{
		Name = e.GetAttributeValue<string>("name"),
		FormJSON = e.GetAttributeValue<string>("formjson"),
		Functions = System.Text.RegularExpressions.Regex.Matches(e.GetAttributeValue<string>("formjson"), "(\"EventName\":(.*?)},)", RegexOptions.None).Cast<Match>().Select(m => m.Value).ToList()
	}).Dump();
}

Since the formjson content is just a string, I used a Regex pattern to find multiple occurrences of the Event Handlers for each form. The result is a list of the EventNames, FunctionNames and LibraryNames for each form that utilises the JavaScript library in the FetchXML statement.

D365 Portals – Language agnostic Content Snippets

When dealing with multi-language websites Content Snippets are a great way to separate out your language specific portal content from your Web Templates and Page Copy.

If you want to use a single Content Snippet across all of your portal languages, for example with generic HTML that is language agnostic, then all you need to do is ensure the value in the Content Snippet Language field is left blank.

D365 Portals – Parent Web Links and Authenticated Users

When you need to display a multi-level web link set the parent level web link does not need to have a page defined. However, this means that the parent level web link will always be visible. If the child web links are behind Restrict-Read web pages, then unauthenticated users will always see the parent level menu option, even if they can’t see the children. Although this is not a major issue as the menu item does not go to a page, it can be confusing to the users if they click on the menu item and it doesn’t go anywhere.

To overcome this problem we can assign a blank page to the parent web link using the Blank Page Page Template. The screenshot below shows the configuration for the parent web link page.
Blank Page

After creating the page we create an Access Control Rule (Restrict Read) that is linked to the Authenticated Users Web Role.
Blank Page - Access Control Rule

Having configured the page, all that is left to do is link the page to the Web Link.
Web Link

Now the Parent Web Link is only visible once the user has logged into the portal and not displayed to unauthenticated users.

D365 Portals – tokenhtml

If you need to refresh the __RequestVerificationToken on your page, this is stored as a hidden input field but can be generated on demand by loading /_layout/tokenhtml.

In my case, I apply the token to a form posting to ensure the request is valid.

    $("#antiforgerytoken").load("/_layout/tokenhtml", function() {
        console.log("tokenhtml loaded");
        $("form>input[name='__RequestVerificationToken']").val($("div#antiforgerytoken>input[name='__RequestVerificationToken']").val());
});

D365 – System Views – Does Not Contain Data

The Advanced Find functionality within D365 v9 allows us to search for records that have no data in a related entity – a NOT IN query. For example, provide a list of Contacts that are not the Primary Contact for an Account.

advanced-find-NOT-IN

However, if we try to create the same using a System View there is currently no option to do a NOT IN query.

system-view-filter

To create a System View which contains a NOT IN query I had to complete the following steps.

  1. Create a basic System View with the required column layout and Publish the View.
  2. Using XrmToolBox and the View Designer Plugin I opened the System View and edited the query (using FetchXML Builder)
  3. Finally, save and publish the changes to the System View.

NOT-IN-FetchXML

A few things I have found when creating NOT IN System Views.

    1. The System View does get created in the list of System Views for the entity and displays the correct content.

system-view-1

    1. When you open the Advanced Find from the System View, the filters for the System View are displayed correctly but the name of the System View we created is not listed.

system-view-2

    1. When you re-open the System View in the D365 solution after you have made the NOT IN modification, the option to Edit the filter criteria is no longer available. Any additional changes to the System View would need to be made using the XrmToolBox View Designer Plugin.

system-view-3