How to call #webapi from #PowerPlatform #Portals
In almost all of my portal projects I get a question from my clients: "How to call an external api from #portals"? This question has been common that I decided to write about my experience on this topic which might be helpful for the community. This post will focus on two main areas:
- The available options to integrate #portals with external
- A step by step guide on one of the least discussed options which is using Oauth Implicit Grant flow and how I created a simple demo for one of my customers
I would like to give a business context to this scenario. Any enterprise solution requires integration and interaction of multiple systems which #portals could be one of them. Imagine a scenario where a customer is looking for a quote on a product in the company portal. In this case the #portal is required to bring quote details from CPQ (Configure Price Quote) system to the portal. In another scenario, a #portal is required to integrate with a core banking system to get the customer's latest balances. In these scenarios and similar ones, we will require the #portal to integrate with an external api to get information.
In order to enable such integrations, the #portal must be able to make calls in a secure way as most of the internal systems require authentication before anything can happen. So what are the options available?
Solutions
Since #powerplatform #portals are tightly integrated with #powerplatform, in most cases the integration is done through the #powerplatform itself. However, the integration through these #powerplatform has three flavors.
- The first one is creating actions in the platform which communicated with external API and manages the requests and responses; then calling the actions through a workflow where the workflow is triggered using Entity Form or Entity List events.
- The second option is to use #MicrosoftFlow to encapsulate the Workflow and Action part in a Flow. The benefit of this solution is that you won't need to write code (in most cases but not guaranteed) to call #webapi.
- The above two options,
use #PowerPlatform to facilitate the integration and all calls are routed
through the platform. However, going through the server is not always
feasible. There are situations in which you would like to make client side
calls from javascript using Ajax from #portals to call external API.
However, the main concerns in these scenarios are authentication. And the
solution provided by the platform is "Oauth Implicit Grant Flow".
If you would like to learn more about what is the "Oauth Implicit Grant Flow" beyond the #PowerPlatform, you can read more here.
There are concerns over the Oauth Implicit Grant flow and the recommendation is to use "Oauth code grant flow". According to the Oauth working group, "t is generally not recommended to use the implicit flow (and some servers prohibit this flow entirely). In the time since the spec was originally written, the industry best practice has changed to recommend that public clients should use the authorization code flow with the PKCE extension instead.". Microsoft is aware of this restriction however, it is believed Oath implicit grant flow is still ok to use.
I have proposed an idea to implement the Oauth code grant flow in this IDEA. Please vote for it.
Now getting back to
the topic: How to Integrate:
In this scenario, there is no server side calls are required. A complete documentation is available here. However, the documentation is not very helpful if you want to do things quickly since there is a learning cycle involved. OAuth 2.0 implicit grant flow supports endpoints that a client can call to get an ID token. Two endpoints are used for this purpose: authorize and token. I will not go to the details of these calls and I assume you already know what these are.
So here is what you will have to do:
So here is what you
will have to do:
- Create your web api. You can
download the sample api from this Github project. This website is no different than any MVP website. So
you can create your own with Web APIs.
- Next is to register your
application in Azure Active Directory. This is a free service which you
can use to provide authentication to your web api. A step by step details
of the registration process is in this link.
The REDIRECT URL must be the direct link to the page you created in the step # 2. You will need to note the following after this step:
- Client ID
- Redirect URL
- Let's say you have a Quote page in your portal and you would like to place a button on the portal page to get Quotations from your internal website. You will have to put a custom HTML in your "Content Page" (not the main page) of the portal. This custom HTML will be used to add a QUOTE button to the portal and also retrieve the Quotation by use of a custom javascrtip code.
<h2>The
QUOTE BUTTON</h2>
<button
type="button" onclick="callAuthorizeEndpoint()">Give me
a Quote!</button>
<script>
//Remove
this line to avoid State validation
$.cookie("useStateValidation",1);
function
callAuthorizeEndpoint(){
//Used
for State validation
var
useStateValidation = $.cookie("useStateValidation");
var
appStateKey = 'my@pp$tate';
var
sampleAppState = {id:500, name:"logic"};
//Replace
with Client Id Registered on CRM
var
clientId = "CLIENT ID OBTAINED FROM AZURE ACTIVE DIRECTORY";
//Replace
with Redirect URL registered on CRM
var
redirectUri =
encodeURIComponent("https://MYPORTAL.powerappsportals.com/REDIRECT_PAGE/");
//Authorize
Endpoint
var
redirectLocation =
`/_services/auth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}`;
//Save
state in a cookie if State validation is enabled
if(useStateValidation){
$.cookie(appStateKey,
JSON.stringify(sampleAppState));
redirectLocation
= redirectLocation + `&state=${appStateKey}`;
console.log("Added
State Parameter");
}
//Redirect
window.location
= redirectLocation;
}
</script>
- Modify the source code in the web api website to use the Client ID and Redirect URL in its startup page.
public
virtual Task ValidateIdentity(OAuthValidateIdentityContext context)
{
try
{
if
(!context.Request.Headers.ContainsKey("Authorization"))
{
return
Task.FromResult<object>(null);
}
//
Retrieve the JWT token in Authorization Header
var
jwt = context.Request.Headers["Authorization"].Replace("Bearer
", string.Empty);
var
handler = new JwtSecurityTokenHandler();
var
token = new JwtSecurityToken(jwt);
var
claimIdentity = new ClaimsIdentity(token.Claims,
DefaultAuthenticationTypes.ExternalBearer);
var
param = new TokenValidationParameters
{
ValidateAudience
= false, // Make this false if token was generated without clientId
ValidAudience
= "CLIENT ID", //Replace with Client Id Registered on CRM. Token
should have been fetched with the same clientId.
ValidateIssuer
= true,
IssuerSigningKey
= _signingKey,
IssuerValidator
= (issuer, securityToken, parameters) =>
{
var
allowed = GetAllowedPortal().Trim().ToLowerInvariant();
if
(issuer.ToLowerInvariant().Equals(allowed))
{
return
issuer;
}
throw
new Exception("Token Issuer is not a known Portal");
}
};
SecurityToken
validatedToken = null;
handler.ValidateToken(token.RawData,
param, out validatedToken);
var
claimPrincipal = new ClaimsPrincipal(claimIdentity);
context.Response.Context.Authentication.User
= claimPrincipal;
context.Validated(claimIdentity);
}
catch(Exception
exception)
{
System.Diagnostics.Debug.WriteLine(exception);
return
null;
}
return
Task.FromResult<object>(null);
}
- The next step is to use Custom HTML on the Redirect PAGE so that you can make the call to the Web API by the token obtained in this step.
function
getResultInUrlFragment(hash){
if(hash){
var result = {};
hash.substring("1").split('&').forEach(function(keyValuePair){
var arr = keyValuePair.split('=');
// Add to result, only the keys with values
arr[1] && (result[arr[0]] =
arr[1]);
});
return
result;
}
else{
return
null;
}
}
//Validate
State parameter
//Returns
true for valid state and false otherwise
function
validateState(stateInUrlFragment){
if(!stateInUrlFragment){
console.error("State
Validation Failed. State parameter not found in URL fragment");
return
false;
}
//
State parameter in URL Fragment doesn't have a corresponding cookie.
if(!$.cookie(stateInUrlFragment)){
console.error("State
Validation Failed. Invalid state parameter");
return
false;
}
return
true;
}
var
useStateValidation = $.cookie("useStateValidation");
var
appState = null;
//Fetch
the parameters in Url fragment
var
authorizeEndpointResult = getResultInUrlFragment(window.location.hash);
//Validate
State
if(useStateValidation){
if(!validateState(authorizeEndpointResult.state)){
authorizeEndpointResult
= null;
}
else{
appState
=
$.cookie(authorizeEndpointResult.state);
console.log("State:
"+appState);
}
}
//Display
token
if(authorizeEndpointResult){
var data = authorizeEndpointResult.token;
console.log("Token:"
+ data);
$.ajax({
type:
"GET",
contentType:
"application/json; charset=utf-8",
dataType:
"json",
headers:
{
Accept:"text/plain;
charset=utf-8",
"Authorization": "Bearer
"+data
},
success:
function (data) {
alert(JSON.stringify(data));
console.log(data);
},
//End of AJAX Success function
failure:
function (data) {
alert(data.responseText);
},
//End of AJAX failure function
error:
function (data) {
alert(data.responseText);
}
//End of AJAX error function
});
}
I hope this post helps you a bit to make your portals connect to the outside world!
Thank you . It is good information and really helps others in understanding/implementing external systems with portal.
ReplyDeleteThank you Sharan. Happy to see it was usful for you.
Delete