CRUD operation on Single Page Application for D365 using MSAL with jqGrid

We are re-creating our SPA but using MSAL instead of ADAL. MSAL allow authentication using wider set of identities instead of just Azure AD as for ADAL. The list of differences can be found here.

Below is the sample code for performing CRUD operation in jqGrid for D365 using MSAL.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/themes/redmond/jquery-ui.min.css">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/free-jqgrid/4.15.4/css/ui.jqgrid.min.css">
    <a href="https://alcdn.msauth.net/browser/2.0.0-beta.4/js/msal-browser.js">https://alcdn.msauth.net/browser/2.0.0-beta.4/js/msal-browser.js</a>
    <a href="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js">https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js</a>
    <a href="https://cdnjs.cloudflare.com/ajax/libs/free-jqgrid/4.15.4/jquery.jqgrid.min.js">https://cdnjs.cloudflare.com/ajax/libs/free-jqgrid/4.15.4/jquery.jqgrid.min.js</a>
    <script type="text/javascript">
        const msalConfig = {
            auth: {
                clientId: "xxxxxxxxx-f8d4-448b-ba96-e3097cd7c3dd",
                authority: "https://login.microsoftonline.com/common",
                redirectUri: "https://localhost:44308/SinglePageAppMSAL.html"
            },
            cache: {
                cacheLocation: "sessionStorage", // This configures where your cache will be stored
                storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge
            },
            system: {
                loggerOptions: {
                    loggerCallback: (level, message, containsPii) => {
                        if (containsPii) {
                            return;
                        }
                        switch (level) {
                            case msal.LogLevel.Error:
                                console.error(message);
                                return;
                            case msal.LogLevel.Info:
                                console.info(message);
                                return;
                            case msal.LogLevel.Verbose:
                                console.debug(message);
                                return;
                            case msal.LogLevel.Warning:
                                console.warn(message);
                                return;
                        }
                    }
                }
            }
        };

        const loginRequest = {
            scopes: ["https://xxxxxxxxxxxxx.crm.dynamics.com/.default"]
        };


        const tokenRequest = {
            scopes: ["https://xxxxxxxxxxxxx.crm.dynamics.com/.default"],
            forceRefresh: false // Set this to "true" to skip a cached token and go to the server to get a new token
        };


        const myMSALObj = new msal.PublicClientApplication(msalConfig);
        let accessToken;
        let username = "";

        myMSALObj.handleRedirectPromise().then(handleResponse).catch(err => {
            console.error(err);
        });

        function handleResponse(resp) {
            if (resp !== null) {
                username = resp.account.username;
                loginButton.style.display = "none";
                logoutButton.style.display = "block";
                getAccountsButton.style.display = "block";

                var helloMessage = document.createElement("p");
                helloMessage.textContent = "Hello " + username;
                message.appendChild(helloMessage);
            }
            else {
                const currentAccounts = myMSALObj.getAllAccounts();
                if (currentAccounts === null) {

                    return;
                } else if (currentAccounts.length > 1) {
                    // Add choose account code here
                    console.warn("Multiple accounts detected.");
                } else if (currentAccounts.length === 1) {
                    username = currentAccounts[0].username;
                }
            }
        }

        function signIn() {
            myMSALObj.loginRedirect(loginRequest);            
        }

        function signOut() {
            const logoutRequest = {
                account: myMSALObj.getAccountByUsername(username)
            };
            myMSALObj.logout(logoutRequest);
        }

        function getTokenRedirect(request) {
            request.account = myMSALObj.getAccountByUsername(username);
            return myMSALObj.acquireTokenSilent(request).catch(error => {
                console.warn("silent token acquisition fails. acquiring token using redirect");
                if (error instanceof msal.InteractionRequiredAuthError) {
                    return myMSALObj.acquireTokenRedirect(request);
                } else {
                    console.warn(error);
                }
            });
        }

        function readAccount() {
            getTokenRedirect(tokenRequest).then(response => {
                retrieveAccounts(response.accessToken);
            }).catch(error => {
                console.error(error);
            });
        }

        function renderAccount(accounts, token) {
            $("#grid").clearGridData();
            var listOfAccounts = [];
            accounts.forEach(function (account) {
                var name = account.name;
                var city = account.address1_city;
                var accountid = account.accountid;
                listOfAccounts.push({
                    name: name,
                    city: city,
                    accountid: accountid
                });
            });
            $("#grid").jqGrid({
                colModel: [
                    { name: "name", width: 300, editable: true },
                    { name: "city", width: 300, editable: true },
                    { name: "accountid", width: 300, editable: true, editrules: { edithidden: false }, hidden: true },
                ],
                pager: '#pager2',
                sortname: 'name',
                viewrecords: true,
                sortorder: "desc",
                caption: "",
                editable: true,
                data: listOfAccounts
            });
            $("#grid").setGridParam({ data: listOfAccounts }).trigger("reloadGrid");
            $("#grid").jqGrid('navGrid', '#pager2',
                {
                    edit: true,
                    add: true,
                    del: true,
                    search: false,
                    refresh: false,
                    view: false
                },
                {
                    editCaption: "Edit Account",
                    edittext: "Edit",
                    closeOnEscape: true,
                    closeAfterEdit: true,
                    savekey: [true, 13],
                    errorTextFormat: commonError,
                    reloadAfterSubmit: true,
                    onclickSubmit: function (response, postdata) {
                        if (confirm('Are you sure you want to update this row?')) {
                            UpdateAccount(postdata, token);
                            return [true, ''];
                        } else {
                            return [false, 'You can not submit!'];
                        }
                    }
                },
                {
                    addCaption: "Add Account",
                    addtext: "Add Account",
                    closeAfterAdd: true,
                    recreateForm: true,
                    errorTextFormat: commonError,
                    onclickSubmit: function (response, postdata) {
                        postdata.accountid = AddAccount(postdata, token);
                    }
                },
                {
                    deleteCaption: "delete Account",
                    deletetext: "Delete Account",
                    closeOnEscape: true,
                    closeAfterEdit: true,
                    savekey: [true, 13],
                    errorTextFormat: commonError,
                    reloadAfterSubmit: true,
                    onclickSubmit: function (response, postdata) {
                        var sr = jQuery("#grid").getGridParam('selrow');
                        var rowdata = jQuery("#grid").getRowData(sr);
                        DeleteAccount(rowdata, token);
                    }
                },
                {},
            );
        }

        function retrieveAccounts(token) {
            if (!token) {
                errorMessage.textContent = 'error occurred: ';
                return;
            }
            
            var req = new XMLHttpRequest()
            req.open("GET", encodeURI("https://xxxxxxxxxxxxx.crm.dynamics.com/api/data/v9.1/accounts?$select=name,address1_city,accountid&$top=10"), true);
            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 account = JSON.parse(this.response).value;
                        renderAccount(account, token);
                    }
                    else {
                        var error = JSON.parse(this.response).error;
                        console.log(error.message);
                        errorMessage.textContent = error.message;
                    }
                }
            };
            req.send();
        }


        function UpdateAccount(params, token) {
            var req = new XMLHttpRequest();
            req.open("PATCH", encodeURI("https://xxxxxxxxxxxxx.crm.dynamics.com/api/data/v9.1/accounts(" + params.accountid + ")"), false);
            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");
            var body = JSON.stringify({
                "name": params.name,
                "address1_city": params.city
            });
            req.send(body);
            if (req.readyState === 4) {
                if (req.status === 204) {
                    var uri = req.getResponseHeader("OData-EntityId");
                    var regExp = /\(([^)]+)\)/;
                    var matches = regExp.exec(uri);
                    var accountid = matches[1];
                    alert(params.name + " Updated");
                }
                else {
                    var error = JSON.parse(req.response).error;
                    alert(error.message);
                }
            }
        }

        function AddAccount(params, token) {
            var account = {};
            account["name"] = params.name;
            account["address1_city"] = params.city;
            var req = new XMLHttpRequest();
            req.open("POST", encodeURI("https://xxxxxxxxxxxxx.crm.dynamics.com/api/data/v9.1/accounts"), false);
            req.setRequestHeader("Authorization", "Bearer " + token);
            req.setRequestHeader("OData-MaxVersion", "4.0");
            req.setRequestHeader("OData-Version", "4.0");
            req.setRequestHeader("Accept", "application/json");
            req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
            req.send(JSON.stringify(account));
            if (req.readyState === 4) {
                if (req.status === 204) {
                    var uri = req.getResponseHeader("OData-EntityId");
                    var regExp = /\(([^)]+)\)/;
                    var matches = regExp.exec(uri);
                    var accountid = matches[1];
                    alert(params.name + " Created");
                    return accountid;
                }
                else {
                    var error = JSON.parse(req.response).error;
                    alert(error.message);
                }
            }
        }

        function DeleteAccount(params, token) {
            var req = new XMLHttpRequest();
            req.open("DELETE", encodeURI("https://xxxxxxxxxxxxx.crm.dynamics.com/api/data/v9.1/accounts(" + params.accountid + ")"), false);
            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.send();
            if (req.readyState === 4) {
                if (req.status === 204) {
                    alert(params.name + " Deleted");
                }
                else {
                    var error = JSON.parse(req.response).error;
                    alert(error.message);
                }
            }
        }

        function commonError(data) {
            return "Error Occured during Operation. Please try again";
        }
        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");

                //Event handlers on DOM elements
                loginButton.addEventListener("click", signIn);
                logoutButton.addEventListener("click", signOut);
                getAccountsButton.addEventListener("click", readAccount);

                if (username) {
                    loginButton.style.display = "none";
                    logoutButton.style.display = "block";
                    getAccountsButton.style.display = "block";

                    var helloMessage = document.createElement("p");
                    helloMessage.textContent = "Hello " + username;
                    message.appendChild(helloMessage)

                }
                else {
                    loginButton.style.display = "block";
                    logoutButton.style.display = "none";
                    getAccountsButton.style.display = "none";
                }
            }
        }
    </script>
    <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>
    <div id="errorMessage"></div>
    <div id="message"></div>
    <table id="grid"></table>
    <div id="pager2"></div>
</body>
</html>

Single Page Application(SPA) – CRUD operation in D365 using jqGrid

Below is the sample code to create a SPA to perform CRUD operation on D365 using WebAPI and ADAL for authentication. The code demonstrate use of GET, POST, PATCH and DELETE for Retrieve, Create, Update and Delete respectively. The sample code provided from Microsoft is used here as base and additional feature are added on top of it.

<!DOCTYPE html>
<html>
<head>
    <title>D365 Single Page Application for D365 using jqGrid</title>
    <meta charset="utf-8" />
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/themes/redmond/jquery-ui.min.css">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/free-jqgrid/4.15.4/css/ui.jqgrid.min.css">
    <a href="https://secure.aadcdn.microsoftonline-p.com/lib/1.0.17/js/adal.min.js">https://secure.aadcdn.microsoftonline-p.com/lib/1.0.17/js/adal.min.js</a>
    <a href="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js">https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js</a>
    <a href="https://cdnjs.cloudflare.com/ajax/libs/free-jqgrid/4.15.4/jquery.jqgrid.min.js">https://cdnjs.cloudflare.com/ajax/libs/free-jqgrid/4.15.4/jquery.jqgrid.min.js</a>

    <script type="text/javascript">
        "use strict";
        var organizationURI = "https://xxxx.crm.dynamics.com"; //The URL of your Dataverse organization
        var tenant = "xxxx.onmicrosoft.com"; //The name of the Azure AD organization you use
        var clientId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"; //The ClientId you got when you registered the application
        var pageUrl = "https://localhost:44347/SHtmlPage.html"; //The URL of this page in your development environment when debugging.

        var user, authContext, message, errorMessage, loginButton, logoutButton, getAccountsButton;

        //Configuration data for AuthenticationContext
        var endpoints = {
            orgUri: organizationURI
        };

        window.config = {
            tenant: tenant,
            clientId: clientId,
            postLogoutRedirectUri: pageUrl,
            endpoints: endpoints,
            cacheLocation: 'localStorage',
        };

        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");

                //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();
        }

        //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/v9.1/accounts";
            message.appendChild(retrievingAccountsMessage)

            // Function to perform operation is passed as a parameter to the acquireToken method
            authContext.acquireToken(organizationURI, retrieveAccounts)
        }

        //Function that actually retrieves the accounts
        

        function renderAccounts(accounts, token) {
            var listOfAccounts = [];
            accounts.forEach(function (account) {
                var name = account.name;
                var city = account.address1_city;
                var accountid = account.accountid;
                listOfAccounts.push({
                    name: name,
                    city: city,
                    accountid: accountid
                });
            });

            $("#grid").jqGrid({
                colModel: [
                    { name: "name", width: 300, editable: true},
                    { name: "city", width: 300, editable: true },
                    { name: "accountid", width: 300, editable: true, editrules: { edithidden: false }, hidden: true },
                ],
                pager: '#pager2',
                sortname: 'name',
                viewrecords: true,
                sortorder: "desc",
                caption: "",
                editable: true,
                data: listOfAccounts
            });
            $("#grid").jqGrid('navGrid', '#pager2',
                {
                    edit: true,
                    add: true,
                    del: true,
                    search: false,
                    refresh: false,
                    view: false
                },
                {
                    editCaption: "Edit Account",
                    edittext: "Edit",
                    closeOnEscape: true,
                    closeAfterEdit: true,
                    savekey: [true, 13],
                    errorTextFormat: commonError,
                    reloadAfterSubmit: true,
                    onclickSubmit: function (response, postdata) {
                        if (confirm('Are you sure you want to update this row?')) {
                            UpdateAccount(postdata, token);
                            return [true, ''];
                        } else {
                            return [false, 'You can not submit!'];
                        }
                    }
                },
                //add Options. save key parameter will keybind the Enter key to submit.
                {
                    addCaption: "Add Account",
                    addtext: "Add Account",
                    closeAfterAdd: true,
                    recreateForm: true,
                    errorTextFormat: commonError,
                    onclickSubmit: function (response, postdata) {
                        postdata.accountid = AddAccount(postdata, token);
                    }
                },
                {
                    deleteCaption: "delete Account",
                    deletetext: "Delete Account",
                    closeOnEscape: true,
                    closeAfterEdit: true,
                    savekey: [true, 13],
                    errorTextFormat: commonError,
                    reloadAfterSubmit: true,
                    onclickSubmit: function (response, postdata) {
                        var sr = jQuery("#grid").getGridParam('selrow');
                        var rowdata = jQuery("#grid").getRowData(sr);
                        DeleteAccount(rowdata, token);
                    }
                },
                {},
            );
        }
        
        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/v9.1/accounts?$select=name,address1_city,accountid&$top=10"), true);
            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, token);
                    }
                    else {
                        var error = JSON.parse(this.response).error;
                        console.log(error.message);
                        errorMessage.textContent = error.message;
                    }
                }
            };
            req.send();
        }

        function UpdateAccount(params, token) {    
            // Handle ADAL Errors.
            if (error || !token) {
                errorMessage.textContent = 'ADAL error occurred: ' + error;
                return;
            }
            var req = new XMLHttpRequest();
            req.open("PATCH", encodeURI(organizationURI + "/api/data/v9.1/accounts(" + params.accountid + ")"), false);
            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");
            var body = JSON.stringify({
                "name": params.name,
                "address1_city": params.city
            });
            req.send(body);
            if (req.readyState === 4) {
                if (req.status === 204) {
                    var uri = req.getResponseHeader("OData-EntityId");
                    var regExp = /\(([^)]+)\)/;
                    var matches = regExp.exec(uri);
                    var accountid = matches[1];
                    alert(params.name + " Updated");
                }
                else {
                    var error = JSON.parse(req.response).error;
                    alert(error.message);
                }
            }
        }

        function AddAccount(params, token) {
            // Handle ADAL Errors.
            if (error || !token) {
                errorMessage.textContent = 'ADAL error occurred: ' + error;
                return;
            }
            var account = {};
            account["name"] = params.name;
            account["address1_city"] = params.city;
            var req = new XMLHttpRequest();
            req.open("POST", encodeURI(organizationURI + "/api/data/v9.1/accounts"), false);
            req.setRequestHeader("Authorization", "Bearer " + token);
            req.setRequestHeader("OData-MaxVersion", "4.0");
            req.setRequestHeader("OData-Version", "4.0");
            req.setRequestHeader("Accept", "application/json");
            req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
            req.send(JSON.stringify(account));
            if (req.readyState === 4) {
                if (req.status === 204) {
                    var uri = req.getResponseHeader("OData-EntityId");
                    var regExp = /\(([^)]+)\)/;
                    var matches = regExp.exec(uri);
                    var accountid = matches[1];
                    alert(params.name + " Created");
                    return accountid;
                }
                else {
                    var error = JSON.parse(req.response).error;
                    alert(error.message);
                }
            }
        }

        function DeleteAccount(params, token) {
            // Handle ADAL Errors.
            if (error || !token) {
                errorMessage.textContent = 'ADAL error occurred: ' + error;
                return;
            }
            var req = new XMLHttpRequest();
            req.open("DELETE", encodeURI(organizationURI + "/api/data/v9.1/accounts(" + params.accountid + ")"), false);
            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.send();
            if (req.readyState === 4) {
                if (req.status === 204) {
                    alert(params.name + " Deleted");
                }
                else {
                    var error = JSON.parse(req.response).error;
                    alert(error.message);
                }
            }
        }

        function commonError(data) {
            return "Error Occured during Operation. Please try again";
        }

    </script>
    <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>
    <div id="errorMessage"></div>
    <div id="message"></div>   
    <table id="grid"></table>
    <div id="pager2"></div>
 </body>
</html>

Impersonation in C# App for D365

We can impersonate a user in console app or any 3rd party code my passing the Guid of impersonated user in the CallerId field of CrmServiceClient object. Below is sample code:

If we are retrieving a FLS field then the impersonating user and impersonated user both should have access to the field. If either doesn’t have access then no value is returned.

static void Main(string[] args)
{
    string ConnectionStringOAuth = @"AuthType = OAuth;
                                     Username = xxxxxx@xxxxxx.onmicrosoft.com;
                                     Password = xxxxxx;
                                     Integrated Security=true;
                                     Url = https://xxxxxx.crm.dynamics.com;
                                     AppId = xxxxxx;
                                     RedirectUri = https://xxxxxxxx;
                                     LoginPrompt=Auto";
    CrmServiceClient svc = new CrmServiceClient(ConnectionStringOAuth);
    svc.CallerId = Guid.Parse("{60FBEAFB-7724-EB11-A813-000D3A569CF5}");

    var cs = svc.RetrieveMultiple(new QueryExpression("ps_configurationsetting")
    {
          ColumnSet = new ColumnSet(true),
          Criteria =   {
                          Filters =
                          {
                                new FilterExpression
                                {
                                   FilterOperator = LogicalOperator.And,
                                   Conditions = {
                                         new ConditionExpression("ps_key", ConditionOperator.Equal, "azurekey")
                                   }
                                 }
                          }
                    }
     });
     string value = cs.Entities.Count != 0 ?
     (cs.Entities[0].Attributes.Contains("ps_value") ?
     cs.Entities[0].Attributes["ps_value"].ToString() : string.Empty) :
 string.Empty;

     Entity task = new Entity("task")
     {
          Attributes = new AttributeCollection()
          {
               new KeyValuePair<string, object>("regardingobjectid",
               new EntityReference("contact", Guid.Parse("{87D4EF7F-DE38-EB11-A813-0022481BFEB4}"))),
               new KeyValuePair<string, object>("subject", value)
          }
     };
     var newRecord = svc.Create(task);
     Console.ReadLine();
}

Possible Error :

contextUserId=<impersonating userid> is missing privilege <Privilege GUID>. Parameter’user’=<impersonated userid>, callerId=<impersonated userid>.

This error will occur if privilege prvActOnBehalfOfAnotherUser is missing on impersonating user.

Impersonation using JavaScript in D365

We can also impersonate a user using javascript in WebAPI by just passing the callerId in header. The impersonator should have the delegate role ( prvActOnBehalfOfAnotherUser) privilege.

impersonateuser

Below is the sample code for that.

var entity = {};
        entity.subject = value;
        entity["regardingobjectid_contact@odata.bind"] = "/contacts(" + recordId + ")";

        var req = new XMLHttpRequest();
        req.open("POST", serverUrl + "/api/data/v9.0/tasks", true);
        req.setRequestHeader("OData-MaxVersion", "4.0");
        req.setRequestHeader("OData-Version", "4.0");
        req.setRequestHeader("Accept", "application/json");
        req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
        req.setRequestHeader("Prefer", "odata.include-annotations=\"*\"");
        req.setRequestHeader("MSCRMCallerID", "60FBEAFB-7724-EB11-A813-000D3A569CF5");

        req.onreadystatechange = function () {
            if (this.readyState === 4) {
                req.onreadystatechange = null;
                if (this.status === 204) {
                    Xrm.Utility.alertDialog("New Task Record Created with Related Contact");
                } else {
                    Xrm.Utility.alertDialog(this.statusText);
                }
            }
        };
        req.send(JSON.stringify(entity));

We get a task created with following audit :

If a field is having FLS (Field level security) enabled then it won’t allow access even if the impersonated user has full access but impersonating user doesn’t have access to the field.

Below is the full code where we trigger a Create Task on click on ribbon button. We read the value from custom entity ps_configurationsetting and then create task with the same subject. If the calling user doesn’t have access to FLS, impersonation wont help too.

ContactRibbon = {
    CreateTask: function (context) {
        var formContext = context;
        var recordId = formContext.data.entity.getId().replace("{", "").replace("}", "");
        var globalContext = Xrm.Utility.getGlobalContext();
        var serverUrl = globalContext.getClientUrl();

        var url = Xrm.Page.context.getClientUrl() + "/api/data/v9.1/ps_configurationsettings?$filter=ps_key eq 'azurekey'";
        var reqCS = new XMLHttpRequest();

        reqCS.open("GET", url, false);
        reqCS.setRequestHeader("Accept", "application/json");
        reqCS.setRequestHeader("Content-Type", "application/json; charset=utf-8");
        reqCS.setRequestHeader("MSCRMCallerID", "60FBEAFB-7724-EB11-A813-000D3A569CF5");
        reqCS.send();

        if (reqCS.readyState == 4) {
            if (reqCS.status == 200) {
                var data = JSON.parse(reqCS.response);
                if (data != null && data.value.length > 0) {
                    var value = data.value[0].ps_value;
                }
            }
            else {
                alert("Error: " + reqCS.responseText);
            }
        }
        var entity = {};
        entity.subject = value;
        entity["regardingobjectid_contact@odata.bind"] = "/contacts(" + recordId + ")";

        var req = new XMLHttpRequest();
        req.open("POST", serverUrl + "/api/data/v9.0/tasks", true);
        req.setRequestHeader("OData-MaxVersion", "4.0");
        req.setRequestHeader("OData-Version", "4.0");
        req.setRequestHeader("Accept", "application/json");
        req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
        req.setRequestHeader("Prefer", "odata.include-annotations=\"*\"");
        req.setRequestHeader("MSCRMCallerID", "60FBEAFB-7724-EB11-A813-000D3A569CF5");
        req.onreadystatechange = function () {
            if (this.readyState === 4) {
                req.onreadystatechange = null;
                if (this.status === 204) {
                    Xrm.Utility.alertDialog("New Task Record Created with Related Contact");
                } else {
                    Xrm.Utility.alertDialog(this.statusText);
                }
            }
        };
        req.send(JSON.stringify(entity));
    }
};

Impersonation in a plugin D365

We can impersonate any user in plugin code by passing user GUID to CreateOrganizationService of IOrganizationServiceFactory. Below is sample plugin code. The code is impersonating with user GUID and then creating task.

public void Execute(IServiceProvider serviceProvider)
        {
            var tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
            var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));

            if (context.InputParameters.Contains("Target") && context.InputParameters["Target"] is Entity)
            {
                var serviceFactory =
                    (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
                IOrganizationService service;

                try
                {
                    service = serviceFactory.CreateOrganizationService(new Guid("60FBEAFB7724EB11A813000D3A569CF5"));
                    CreateTask(service, context);
                }

                catch (FaultException<OrganizationServiceFault> ex)
                {
                    throw new InvalidPluginExecutionException("An error occurred.", ex);
                }

                catch (Exception ex)
                {
                    tracingService.Trace("Error: {0}", ex.ToString());
                    throw;
                }
            }
        }

In create task function, we are retrieving value from FLS field and then creating a task with that description. If user which we are impersonating doesn’t have access to FLS, we will get ‘Access Denied’ in description.

public void CreateTask(IOrganizationService service, IPluginExecutionContext context)
{
       var cs = service.RetrieveMultiple(new QueryExpression("ps_configurationsetting")
       {
            ColumnSet = new ColumnSet(true),
            Criteria =   {
                                Filters =
                                {
                                    new FilterExpression
                                    {
                                        FilterOperator = LogicalOperator.And,
                                        Conditions = {
                                            new ConditionExpression("ps_key", ConditionOperator.Equal, "azurekey")
                                        }
                                    }
                                }
                    }
        });

        string securevalue = cs.Entities.Count != 0 ?
            (cs.Entities[0].Attributes.Contains("ps_securevalue") ?
            cs.Entities[0].Attributes["ps_securevalue"].ToString() : "Access Denied") :
            string.Empty;

        Entity followupTask = new Entity("task");
        followupTask["subject"] = "Send e-mail .";
        followupTask["description"] = securevalue;
        followupTask["scheduledstart"] = DateTime.Now.AddDays(7);
        followupTask["scheduledend"] = DateTime.Now.AddDays(7);
        followupTask["category"] = context.PrimaryEntityName;
        if (context.OutputParameters.Contains("id"))
        {
            Guid regardingobjectid = new Guid(context.OutputParameters["id"].ToString());
            string regardingobjectidType = "contact";
            followupTask["regardingobjectid"] = new EntityReference(regardingobjectidType, regardingobjectid);
        }
        service.Create(followupTask);
}

Error in plugin registration

System.FormatException: Input string was not in a correct format.    at System.Text.StringBuilder.FormatError() +0xf    at System.Text.StringBuilder.AppendFormatHelper(IFormatProvider provider, String format, ParamsArray args) +0x0    at System.String.FormatHelper(IFormatProvider provider, String format, ParamsArray args) +0xe    at System.String.Format(IFormatProvider provider, String format, Object arg0, Object arg1) +0x0    at Microsoft.Crm.ObjectModel.TargetFrameworkVersionValidator.ValidateInternal() +0x4f    at Microsoft.Crm.ObjectModel.PluginValidatorBase.Validate() +0x7b    at Microsoft.Crm.ObjectModel.PluginAssemblyServiceInternal1.ValidateAssemblyMetadata(ExecutionContext context, IBusinessEntity pluginAssembly, CrmPluginAssemblyMetadata assemblyMetadata) +0x30    at Microsoft.Crm.ObjectModel.PluginAssemblyServiceInternal1.VerifyRegistrationAbility(IBusinessEntity pluginAssembly, Boolean createCall, ExecutionContext context, CrmPluginAssemblyMetadata assemblyMetadata) +0x60    at Microsoft.Crm.ObjectModel.PluginAssemblyServiceInternal`1.ValidateOperation(String operationName, IBusinessEntity entity, ExecutionContext context) +0xa7    at Microsoft.Crm.ObjectModel.SdkEntityServiceBase.CreateInternal(IBusinessEntity entity, ExecutionContext context, Boolean verifyAction) +0x3: Microsoft Dynamics CRM has experienced an error. Reference number for administrators or support: #03F5A444

This error happens when our Visual Studio Plugin project was in 4.7.2 while CRM plugin should be in 4.6.2

Change the VS project to 4.6.2 and it should work.

Connecting to D365

In Dynamics 365 with O365 authentication (WS-Trust) being deprecated, following are the available options to query Dynamics 365 data using SDK.

We can use CrmServiceClient with WebAPI/Organization Service or directly call WebAPI. Following are possible ways:

  • Certificate
  • OAuth
  • ClientSecret
  1. We need to register app on Azure AD and then create client secret/Certificate.

You can navigate to Azure AD and go to ‘App Registrations’ and then create a new app. Follow the steps as per here :

https://docs.microsoft.com/en-us/powerapps/developer/common-data-service/walkthrough-register-app-azure-active-directory#create-an-application-registration

2. To add Client Secret to the newly created App, please follow below steps :

https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app#add-a-certificate

https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app#add-a-client-secret

To use certificate authentication – Create Self Signed Certificate (if you don’t have it already) and Upload Certificate or install the certificate on your local.

3. Register the App user in Dynamics 365 with App client ID and give proper role.

Authentication with Certificate comparison

Authentication with OAuth comparison

Certificate Authentication

HTTP Body

Certificate with CrmServiceClient (Organization Service)
private static void CertificateCreate()
 {
     string ConnectionStringCertificate = @"AuthType = Certificate;SkipDiscovery=true;Url = https://[orgname].crm.dynamics.com;thumbprint = [ThumbPrint];ClientId = [ClientId];";
     CrmServiceClient svc = new CrmServiceClient(ConnectionStringCertificate);
     var res = svc.Create(new Microsoft.Xrm.Sdk.Entity("contact")
     {
        Attributes = new Microsoft.Xrm.Sdk.AttributeCollection
        {
           new KeyValuePair("firstname","testuserOCertificate"),
           new KeyValuePair("lastname","CrmServiceClient"),
        }
     });
     Console.WriteLine("Certificate Record ID :" + res.ToString());
 }
Certificate with CrmServiceClient (WebAPI)
public static void OAuthCertificateCreate()
 {
     string ConnectionStringOAuth = @"AuthType = Certificate;SkipDiscovery=true;Url = https://[orgname].crm.dynamics.com;thumbprint = [ThumbPrint];ClientId = [ClientId];";
     CrmServiceClient svc = new CrmServiceClient(ConnectionStringOAuth);
     Dictionary> odataHeaders = new Dictionary>
     {
        { "Accept", new List () { "application/json" } },
        { "OData-MaxVersion", new List () { "4.0" } },
        { "OData-Version", new List () { "4.0" } }
     };
     if (svc.IsReady)
     {
        dynamic contact = new { firstname = "testuserWebAPI", lastname = "CrmServiceClient" };
        string jsonContact = JsonSerializer.Serialize(contact);
        HttpResponseMessage httpResponse = svc.ExecuteCrmWebRequest(
        HttpMethod.Post,
        "contacts",
        jsonContact,
        odataHeaders,
        "application/json");
       if (httpResponse.IsSuccessStatusCode)
      {   
          var contactUri = httpResponse.Headers.GetValues("OData-EntityId").FirstOrDefault();
          Console.WriteLine("OAuth Record ID : {0}", contactUri.Split('(').Last().Split(')').First());
      }
      else
      {   
          Console.WriteLine(httpResponse.ReasonPhrase);
      }
    }
}
Certificate without CrmServiceClient (WebAPI)
private static void CertificateWebAPICreate()
 {
     string clientId = "[ClientId]";
     string url = "https://[orgname].crm.dynamics.com";
     var userCredential = new UserPasswordCredential("[username]@[tenant].onmicrosoft.com", "[Password]");
     string apiVersion = "9.1";
     string webApiUrl = $"{url}/api/data/v{apiVersion}/";
     var authParameters = AuthenticationParameters.CreateFromResourceUrlAsync(new Uri(webApiUrl)).Result;
     var authContext = new AuthenticationContext(authParameters.Authority, false);
     ClientAssertionCertificate cl = new ClientAssertionCertificate(clientId, new X509Certificate2(@"C:\d365cert-D365Login-20201126.pfx", "[certificate password]"));
     var authResult = authContext.AcquireTokenAsync(url, cl).GetAwaiter().GetResult();
     var authHeader = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
     using (var client = new HttpClient())
     {
         client.BaseAddress = new Uri(webApiUrl);
         client.DefaultRequestHeaders.Authorization = authHeader;
         JObject contact1 = JObject.Parse(@" 
         {firstname: 'testuserWebAPI', 
         lastname: 'certificate' " + @"}");
         HttpRequestMessage createrequest1 = new HttpRequestMessage(HttpMethod.Post, client.BaseAddress + "contacts");
         createrequest1.Content = new StringContent(contact1.ToString());
         createrequest1.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
         HttpResponseMessage createResponse1 = client.SendAsync(createrequest1, HttpCompletionOption.ResponseHeadersRead).Result;
         if (createResponse1.IsSuccessStatusCode)
         {
               var id = createResponse1.Headers.GetValues("OData-EntityId").First().Split('(').Last().Split(')').First();
               Console.WriteLine("Certificate WebAPI Record ID : {0}", id);
         }
         else
        {
               Console.WriteLine("The request failed with a status of '{0}'",createResponse1.ReasonPhrase);  
        } 
   }
}

OAuth Authentication

HTTP Body

OAuth with CrmServiceClient (Organization Service)
public static void OAuthOrganizationServiceCreate()
 {
     string ConnectionStringOAuth = @"AuthType = OAuth;
     Username = [username]@[tenant].onmicrosoft.com;
     Password = [Password];
     Integrated Security=true;
     Url = https://[orgname].crm.dynamics.com;
     AppId = [ClientId];
     RedirectUri = https://redirectURL;
     LoginPrompt=Auto";
     CrmServiceClient svc = new CrmServiceClient(ConnectionStringOAuth);
     var res = svc.Create(new Microsoft.Xrm.Sdk.Entity("contact")
     {
        Attributes = new Microsoft.Xrm.Sdk.AttributeCollection
        {
          new KeyValuePair("firstname","testuserOrganizationService"),
          new KeyValuePair("lastname","CrmServiceClient"),
        }
     });
     Console.WriteLine("OrganizationService OAuth Record ID : {0}", res.ToString());
 }
OAuth with CrmServiceClient (WebAPI)
public static void OAuthCreate()
 {
     string ConnectionStringOAuth = @"AuthType = OAuth;
     Username = [username]@[tenant].onmicrosoft.com;
     Password = [Password];
     Integrated Security=true;
     Url = https://[orgname].crm.dynamics.com;
     AppId = [ClientId];
     RedirectUri = https://redirectURL;
     LoginPrompt=Auto";
     CrmServiceClient svc = new CrmServiceClient(ConnectionStringOAuth);
     Dictionary> odataHeaders = new Dictionary>
     {
       { "Accept", new List () { "application/json" } },
       { "OData-MaxVersion", new List () { "4.0" } },
       { "OData-Version", new List () { "4.0" } }
     };
     if (svc.IsReady)
     {
       dynamic contact = new { firstname = "testuserWebAPI", lastname = "CrmServiceClient" };
       string jsonContact = JsonSerializer.Serialize(contact);
       HttpResponseMessage httpResponse = svc.ExecuteCrmWebRequest(
       HttpMethod.Post,
       "contacts",
       jsonContact,
       odataHeaders,
       "application/json");
 
      if (httpResponse.IsSuccessStatusCode)
      {    
          var contactUri = httpResponse.Headers.GetValues("OData-EntityId").FirstOrDefault();     
          Console.WriteLine("OAuth Record ID : {0}", contactUri.Split('(').Last().Split(')').First());     
       }     
       else   
       {    
           Console.WriteLine(httpResponse.ReasonPhrase);  
       }    
   }
}
OAuth without CrmServiceClient (WebAPI)
public static void WebAPICreate()
 {
    string clientId = "[ClientId]";
    string url = "https://[orgname].crm.dynamics.com";
    var userCredential = new UserPasswordCredential("[username]@[tenant].onmicrosoft.com", "[Password]");
    string apiVersion = "9.1";
    string webApiUrl = $"{url}/api/data/v{apiVersion}/";
    var authParameters = AuthenticationParameters.CreateFromResourceUrlAsync(new Uri(webApiUrl)).Result;
    var authContext = new AuthenticationContext(authParameters.Authority, false);
    var authResult = authContext.AcquireTokenAsync(url, clientId, userCredential).GetAwaiter().GetResult();
    var authHeader = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
 using (var client = new HttpClient())
    {
      client.BaseAddress = new Uri(webApiUrl);
      client.DefaultRequestHeaders.Authorization = authHeader;
      JObject contact = JObject.Parse(@"{firstname: 'testuserOrganizationService', lastname: 'OAuth' " + @"}");
      HttpRequestMessage createrequest1 = new HttpRequestMessage(HttpMethod.Post, client.BaseAddress + "contacts");
      createrequest1.Content = new StringContent(contact.ToString());
      createrequest1.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
      HttpResponseMessage createResponse1 = client.SendAsync(createrequest1, HttpCompletionOption.ResponseHeadersRead).Result;
      if (createResponse1.IsSuccessStatusCode)
      {
        var id = createResponse1.Headers.GetValues("OData-EntityId").First().Split('(').Last();
        Console.WriteLine("WebAPI Record ID {0}", id);
      }
      else
      {
        Console.WriteLine("The request failed with a status of '{0}'", createResponse1.ReasonPhrase);
      }
    }
 }

Client Secret Authentication

HTTP Body

ClientSecret with CrmServiceClient (Organization Service)
public static void ClientSecretOrganizationServiceCreate()
 {
    string ConnectionStringClientSecret = @"AuthType = ClientSecret;
         Url = https://[orgname].crm.dynamics.com;
         AppId = [ClientId];
         ClientSecret = [ClientSecret]";
    CrmServiceClient svc = new CrmServiceClient(ConnectionStringClientSecret);
    var res = svc.Create(new Microsoft.Xrm.Sdk.Entity("contact")
    {
        Attributes = new Microsoft.Xrm.Sdk.AttributeCollection
        {
           new KeyValuePair("firstname","testuserClientSecret"),
           new KeyValuePair("lastname","CrmServiceClient"),
        }
    });
    Console.WriteLine("ClientSecret Record ID :" + res.ToString());
 }
ClientSecret with CrmServiceClient (WebAPI – NOT ALLOWED)
public static void OAuthCClientSecretCreate()
 {
     string ConnectionStringOAuth = @"AuthType = ClientSecret;
                       Url = https://[orgname].crm.dynamics.com;
                       AppId = [ClientId];
                       ClientSecret = [ClientSecret]";
    CrmServiceClient svc = new CrmServiceClient(ConnectionStringOAuth);   
    Dictionary<string, List<string>> odataHeaders = new Dictionary<string, List<string>> {
     { "Accept", new List<string> () { "application/json" } },
     { "OData-MaxVersion", new List<string> () { "4.0" } },
     { "OData-Version", new List<string> () { "4.0" } }     
    };
    if (svc.IsReady) 
    {
       dynamic contact = new { firstname = "testuserClientSecret", lastname = "CrmServiceClient" };
       string jsonContact = JsonSerializer.Serialize(contact);
       try
      {
         HttpResponseMessage httpResponse = svc.ExecuteCrmWebRequest(
             HttpMethod.Post,
             "contacts",
             jsonContact,
             odataHeaders,
             "application/json");
         if (httpResponse.IsSuccessStatusCode)
         {
             var contactUri = httpResponse.Headers.GetValues("OData-EntityId").FirstOrDefault();
             Console.WriteLine("OAuth Record ID : {0}", contactUri.Split('(').Last().Split(')').First());
         }
         else
         {
             Console.WriteLine(httpResponse.ReasonPhrase);
         }
     }
     catch (Exception ex)
     {
         Console.WriteLine(ex.Message);
     }
 }
}

  Error: Cannot utilize WebAPI for this request, only oAuth or certificate type authentication is supported 

%d bloggers like this: