Fri, 01/15/2021 - 13:30 By Lloyd Sebag
17 comments
PowerApps Portal WebApi Action

PowerApps Portal WebApi Action : 

Recently I had to work on a PowerApps Portal based project. One of the functionalities consisted in having to securely call an external web service. It was therefore not feasible to place this code at client level in JavaScript.

So I looked at the WebApi side, this preview feature proposed by Microsoft which allows you to do CRUD in JS on D365 from Dynamics Portal. Then I thought to myself that it would be ideal to be able to encapsulate my secure code in a plugin of a D365 action and to call this action from Portal. The code would therefore run on the BackEnd side and voila.

I would like to share with you in this article my findings and how I was able to put this in place.

Exploring WebApi 

To start, you have to understand how this feature works, how to set it up and what are the limits.

I'm not going to give you a detailed tutorial on how to set up this feature here. If you need it in detail,  you can use the one of Microsoft.

On the other hand, here are the main points that I can present:

  1. The very first thing to do is to activate the WebApi for the entity for which you want to perform an operation. This is done from the Portal Site Settings
    In the Portal Management app on the left pane, select Site Settings 

    PowerApps Portal WebApi Action
  2. Then, you have to create a new setting that will activate the WebApi for example for Contact entity by using this syntax : Webapi/contact/enabled

    PowerApps Portal WebApi Action
  3. Now, you have to configure and indicate to Portal which fields will be manipulated for WebApi operations. 
    You can do this by creating a new Site Settings like this one : 

    Web API contact fields
    The list of fields separated by comma will be allowed to be used with WebApi. 
     
  4. Last point before coding. You have to configure the permissions for your users that will use WebApi on your Portal Pages. 
    I let you follow this step by step guide by MS that explain clearly how to do that. 

    Note that this WebApi works also for Anonymous users! 

Now we are ready to add some code on Pages that will let us test the WebApi feature and understand more how it works. 

For my developpment, I used the code snippet provided by Microsoft which works perfectly. This code is composed by one global JS method that will handle the AJAX call in a secure way provided by Portal. I will explain this point later : 

So you have simply to paste this code in your function : 

    // Global Ajax Web Api proxy
    (function(webapi, $){
        function safeAjax(ajaxOptions) {
            var deferredAjax = $.Deferred();

            shell.getTokenDeferred().done(function (token) {
                // add headers for AJAX
                if (!ajaxOptions.headers) {
                    $.extend(ajaxOptions, {
                        headers: {
                            "__RequestVerificationToken": token
                        }
                    }); 
                } else {
                    ajaxOptions.headers["__RequestVerificationToken"] = token;
                }
                $.ajax(ajaxOptions)
                    .done(function(data, textStatus, jqXHR) {
                        validateLoginSession(data, textStatus, jqXHR, deferredAjax.resolve);
                    }).fail(deferredAjax.reject); //AJAX
            }).fail(function () {
                deferredAjax.rejectWith(this, arguments); // on token failure pass the token AJAX and args
            });

            return deferredAjax.promise();  
        }
        webapi.safeAjax = safeAjax;
    })(window.webapi = window.webapi || {}, jQuery)

Then you can call this method to do the operation such as Create. 

webapi.safeAjax({
		type: "POST",
		url: "/_api/contacts",
		contentType: "application/json",
		data: JSON.stringify({
			"firstname": "John"
		}),
		success: function (res, status, xhr) {
      //print id of newly created entity record
			console.log("entityID: "+ xhr.getResponseHeader("entityid"))
		}
	});

At this point, you should be able to call operations : Create, Update, Delete but not Retrieve ! I will join this point to my next explaination. 

Dig it! 

getTokenDeferred()

The first thing we can analyze is the structure of the main method safeAjax that calls the WebApi.
Note that this one uses the following code:

shell.getTokenDeferred().done(function(token){
         //alert("Authentication token: ", token);
});

This code is essential. Indeed, it is provided by Portal in order to generate a token which will be used via the header of the web request in order to be authorized to call the WebApi.

$.extend(ajaxOptions, {
                        headers: {
                            "__RequestVerificationToken": token
                        }
                    }); 

This is not actually an authentication token in the proper sense. As one might find with OAuth 2.0 or OIDC protocols. In fact, this token has the following format:
P5g2D8vRyE3aBn7qQKfVVVAsQc853s-naENvpUAPZLipuw0pa_ffBf9cINzFgIRPwsf7Ykjt46ttJy5ox5r3mzpqvmgNYdnKc1125jphQV0NnM5nGFtcXXqoY3RpusTH_WcHPzH4S4l1PmB8Uu7ubZBftqFdxCLC5n-xT0fHcAY1

It should sound familiar to ASP.Net developers :) This is in fact an anti forgery token (XSRF / CSRF) which allows to avoid Cross-Site Request Forgery. 

Smart approach from Microsoft to certify requests using WebApi are coming from the MS Portal site itself and prevent someone from trying to tamper with them.

Endpoint format

The endpoint format is also interesting to discuss. As you can see the path is always composed with "/_api/<EntityName>". This is an endpoint provided by PowerApps Portal itself instead of the classic one of Dynamics 365 like /api/data/v9.1/<EntityName>

This endpoint act as a proxy to handle the incomming requests and give back corresponding responses. By the way, we can also observe that the format of the responses is also unusual, which certainly proves that this endpoint performs some transformation operations.

Responses

The success function of the Web API call for the creation of a new record shows us here that the PK GUID ID is returned not in an object of the body but in a response header named entityId.

success: function (res, status, xhr) {
      //print id of newly created entity record
			console.log("entityID: "+ xhr.getResponseHeader("entityid"))
		}

So if we try to get more information than what the endpoint is willing to send back to us, that won't work. This is also the reason why the retrieve is not supported, certainly for security reasons, but in the case of a retrieve the header does not contain any fields values.

Why not use this to simulate a C # backend ?! 

Custom Dynamics 365 actions ? 

Now that we have understood how to set up the WebApi and how it technically works. The idea is to see if we can call D365 actions with this mechanism.

At the time of writing this article, Microsoft's documentation does not discuss this option.

Let us take as an example a very simple action named: new_actionname with the following specs:

  • Scope : Contacts
  • Input Parameters : Name(string) 
  • Output Parameters : Message(string)
  • Internal Logic : Message = "Hello " + Name 

If we call this action from the Dynamics 365 API by passing it a target on an existing contact and a Name = "Joe" we should get a 200 with a JSON in the body showing: Message = "Hello Joe". 

Now, let's try to do it with PowerApps Portal Web Api.

It is possible to try to craft your own URL on the model of the one that we could use with the Dynamics 365 REST API.

If we therefore extrapolate: /api/data/v9.1/contacts(<GUID>)/Microsoft.Dynamics.CRM.new_actionname to /_api/contacts(<GUID>)/Microsoft.Dynamics.CRM.new_actionname by using the code shown above, it will work! 

However, the only problem that we will see is that we will only get a 200 OK with an empty body and especially response headers not containing our output parameter named Message.

This is the main limitation of this model. On the other hand, errors and exceptions causing returns of 400 and other than 200 will be displayed correctly in the body, thus giving the possibility of returning information in the event of an error.

Important point: For this call to work properly, there is an essential step to do in the configuration of the Site Settings Portal. You have to return to the Webapi / contact / fields record and add the input parameters of your action, as if they were fields of the entity.
Indeed Portal must surely check if the user has rights on these fields in relation to this configuration.

PowerApps Portal WebApi Action

For information, I opened a Microsoft ticket to ask for explanations on it. They confirmed to me that the use of actions was supported although not documented.

Conclusion

Here is a very interesting feature, which will allow you not only to CREATE, UPDATE and DELETE on your D365 records but also to call Actions with the limitation of having an empty response, but at least with the indication of success or failure.

Within an architecture and solution design, this might not be too blocking and could clearly help to set up an execution strategy for the logic of your PowerApps Portal processing in backend, secure and fully integrated.

PowerApps Portal WebApi Action

Comments

Great blog Llyod! and Thank you for in-depth details.

I am trying to call a global custom action, but getting 401 error(authentication with bearer token is failing).
Is there a way to enable WebApi for this, when I don't have any entity associated.

Thu, 03/04/2021 - 12:15
Khushi (not verified)

Thx Khushi! 

By global custom action you mean an action with None as Entity Scope ? 

In this case I'm afraid that this trick explain on my post will not work because you should let the Web Api autorize your call by configuring an entity permission related to the Action Entity (in the portal configuration).  

Fri, 03/05/2021 - 09:56

In reply to by Khushi (not verified)

Hi Lloyd,
I am trying to call custom action which is not on any entity and i am unable to do so. If you have tried that code, could you please share some insights. The reason been i am calling global action is when ever i calling entity create api from portals the create user is always system but we are having employee self service portal and we want contact (who indeed is a user) to be createdby user

Tue, 11/09/2021 - 21:41
Vamsi (not verified)

In reply to by Lloyd Sebag

Hi LIoyd, thanks for sharing. Can you please post the complete Ajax call with the action URL ? I am not clear where the action URL must be referenced in the code snippet above.

Thu, 03/25/2021 - 12:11
Ray (not verified)

Hi Ray,

Sure, please find the code snippet :

webapi.safeAjax({
          type: "POST",
          url: "/_api/<name_of_action_scope_entity>(<guid_of_record>)/Microsoft.Dynamics.CRM.<name_of_action>",
          contentType: "application/json; charset=utf-8",
          datatype: "json",
          data: JSON.stringify(parameters),
          success: function (res, status, xhr) {
              // You logic here  
          },
            error: function (XMLHttpRequest, textStatus, errorThrown) {
                // Manage Error
            }
        });

 

Tue, 04/06/2021 - 17:11

In reply to by Ray (not verified)

Hi,
I have a output parameter in "Action" and how to get output parameter value in success. Please help me with this.

Wed, 04/14/2021 - 19:48
Lahari (not verified)

Hi Lahari, that's the point, for the moment, you cannot get the output parameters value. You get only get the global 200 OK of the action call. But if you need to get output from your call, you have to consider to use something else since this is a limitation. Sorry. 

Thx 

Mon, 04/19/2021 - 17:17

In reply to by Lahari (not verified)

The portalwebapi(url/_api/contact) not wokring for me
Everything configured as per the doc.
TIA

Thu, 05/06/2021 - 08:24
santhosh (not verified)

Hi I followed the instructions. in the webpage i used this code to run. i am getting the token. however, the api call return the status to 0 first time and second time 401. Contact record is not created

$(document).ready(function () {
shell.getTokenDeferred().done(function (s) {
alert(s + "===="); //this return value
debugger;
//create record
var inputJson = {};
inputJson.firstname = "First name -Test 2";
inputJson.lastname = "Web API 2";
inputJson.emailaddress1 = "abc@hotmail.com";

var inputstring = JSON.stringify(inputJson);
var url = "https://i ave removed this part/api/data/v9.2/contacts";
var xmlHttp = new XMLHttpRequest();
xmlHttp.open("POST", url, true);

xmlHttp.onreadystatechange = function () {
alert(this.status + "===this.status="); // this return 0, 401
if (this.readyState === 4) {
if (this.status === 201) {//created
// perform your logic here.
}
}
};

xmlHttp.setRequestHeader('Content-Type', 'application/json');
xmlHttp.setRequestHeader('__RequestVerificationToken', s);

xmlHttp.send(inputJson);
});
} );

Sun, 05/09/2021 - 23:45
Siva (not verified)

Hi Siva, 

Did you follow the point 4 of the chapter Exploring Web Api ? 

You have to configure the permission of the portal user (auth or anonymous) https://docs.microsoft.com/en-us/powerapps/maker/portals/web-api-perform-operations#step-2---configure-permissions 

Lloyd 

Mon, 05/10/2021 - 17:24

In reply to by Siva (not verified)

Hi Llyod,

I have a simple action that receives a string parameter and performs operation. I am trying to call it from portal, followed the same process mentioned above but getting 500 internal server error. Site Settings, Entity Permissions are all set. Any idea on what I am missing?

"{"error":{"code":"9004010A","message":"An unexpected error occurred while processing the request"}}"

var inputJson = {};
inputJson.AppIds = "Ids";

webapi.safeAjax({
type: "POST",
url: "/_api/entitypluralname(FAB598BD-FA6B-4524-AF75-76D2D97D0818)/Microsoft.Dynamics.CRM.new_TestActionFromPortal",
contentType: "application/json; charset=utf-8",
datatype: "json",
data: JSON.stringify(inputJson),
// data: {"StringIn":"123ABC"},
success: function (res, status, xhr) {
alert("Success");
},
error: function (XMLHttpRequest, textStatus, errorThrown) {
alert(errorThrown);
}
});

Thu, 05/20/2021 - 09:14
Gopi (not verified)

Hi Gopi, thanks for your feedback ;) 

it will be difficult to say like that but I will check two things : 

  1. Check the JSON you are passing to the request. If it is valid, ... 
  2. Be sure that this action can be called correctly from a regular call for example with D365 WebApi or any other classic way. 

Good luck. :) 

Thu, 05/20/2021 - 17:01

In reply to by Gopi (not verified)

Hi Lloyd,

How can I use portal web api for read operation? GET request throws 404 error. Can you please help.

Mon, 07/19/2021 - 16:11
Rajashree (not verified)

Hi Rajashree, for the moment read is not supported. But as answered to Paul it will be soon supported by MS. :) 

https://docs.microsoft.com/fr-fr/power-platform-release-plan/2021wave2/power-apps-portals/portals-web-api-support-read-operations

Tue, 07/27/2021 - 13:46

In reply to by Rajashree (not verified)

HI Lloyd,
As you mentioned that retrieving of data is not possible for now, can you please tell the alternative to retrieve the data from dynamics using webAPI.
We have a situation like , we need to pass the user selected value and get the records associated to it. we can do it through fetchXml but its failing because we need to respond the user event.

thanks in advance

Thu, 07/22/2021 - 13:30
Paul Samuel (not verified)

Hi Paul, I'm afraid that will have no other solution with WebApi... But good news, very soon (Oct 2021) you will be able to Read and even call Custom Action thanks to the new features introduces by 2021Wave2 plan. 

https://docs.microsoft.com/fr-fr/power-platform-release-plan/2021wave2/power-apps-portals/portals-web-api-support-read-operations

Best luck. 

Tue, 07/27/2021 - 13:45

In reply to by Paul Samuel (not verified)

Add new comment

Image CAPTCHA
Enter the characters shown in the image.