How to Enforce Login on Editing Pages in MS Power Pages – Part 2
This is the 2nd part of the series. For Part 1, click here.
In Part 1 one of the suggestions was to redirect user to the Logout page when the referrer (i.e page which user came from) was not ciamlogin.com. Ciamlogin is a page which standard Power Pages login mechanism is using. When a custom identity provider is used, the referrer page often does not come from Ciamlogin.
An example of this is Australia’s MyID, which is the government-issued identity authentication mechanism. One of the clients I worked at was using MyID, and when user was redirected back from MyID authentication prompt, the referrer was showing empty. Therefore, using the approach laid out in Part 1 would not work.
The implementation detailed on this section is more suitable. The summary of the steps are as follows:
- Creation of a “session” table
- Creation of 2 custom plugins
- Creation of Custom API through PRT
- Creation of an Intermediary Session Creation page
- Securing the destination page to redirect back to the Intermediary Session Creation Page
In a nutshell, let’s say you want to secure “/profile” page, you would redirect user to “/Intermediary-Session-Creation-Page?VisitedPage=/profile” first. Then, from within the intermediary page, a session token is created. Once the session token is created, user is redirected to the destination page via the logon page. And finally, in the destination page, the session token is checked.
Details are as follows.
Step 1. Creation of “session” table
Create a new Table and call it User Session. Create 2 columns:
- Contact (lookup to Contact table)
- SessionToken (string)
This page will hold the session token information for the destination page the user is visiting. We will use the Name field to hold the URL of the destination page.
Step 2. Creation of 2 custom plugins
In Visual Studio, create 2 plugins for our Custom API:
- CreateUserSession – used to create the session token
- ValidateUserSession – used to validate the session token
For CreateUserSession, my code is something like below. It will take the ContactId and PageUrl input parameter, then it will create a session token (mine is GUID format) and pass it to SessionToken output parameter.
The Session Token string is stored in a record within User Session table we created earlier.
Guid contactId = (Guid)context.InputParameters["ContactId"];
string pageUrl = (string)context.InputParameters["PageUrl"];
if (!String.IsNullOrEmpty(pageUrl) && contactId != Guid.Empty)
{
tracingService.Trace(contactId.ToString());
using (var client = new HttpClient())
{
// Get contact details.
var sessionToken = myRepo.CreateUserSession(pageUrl, contactId);
context.OutputParameters["SessionToken"] = sessionToken;
}
}
For the ValidateUserSession, it will take ContactId, SessionToken and PageUrl input parameters. We will retrieve the existing token from User Session table and compare it with the input. A string “success” is returned through the SessionResponse output parameter when validation is successful.
Guid contactId = (Guid)context.InputParameters["ContactId"];
Guid sessionToken = (Guid)context.InputParameters["SessionToken"];
string pageUrl = (string)context.InputParameters["PageUrl"];
if (sessionToken != Guid.Empty && contactId != Guid.Empty &&
!String.IsNullOrEmpty(pageUrl))
{
tracingService.Trace(contactId.ToString());
using (var client = new HttpClient())
{
// Get contact details.
var existingSession = myRepo.GetUserSession(pageUrl, contactId);
if(existingSession != null)
{
if(Guid.Parse(existingSession.SessionToken) == sessionToken)
{
// Delete the session token.
myRepo.DeleteUserSession(existingSession.SessionId);
context.OutputParameters["SessionResponse"] = "success";
}
}
}
}Register your plugin with Plugin Registration Tool (PRT).
Step 3. Creation of Custom API through PRT
Next, create a Custom API to generate the session token. We will be using Marius Wodtke’s PowerPagesActions solution for this. You can read how I utilise it for CAPTCHA implementation on this article. Basically, the solution allows you to call a Custom API created in Dataverse, within Power Pages. Without it, you will not be able to do so easily due to authentication issue.
Using Plugin Registration Tool (PRT), click Register > Register New Custom API.
Name it “CreateUserSession”. It will have:
- 2 Request Parameters
- ContactId (GUID)
- PageUrl (string)
- 1 Response Parameter
- SessionToken (string)
Assign the CreateUserSession to the plugin you created earlier.
Then, do another one for the ValidateUserSession. With this one, we will need the following parameters:
- 3 Request Parameters
- ContactId (GUID)
- PageUrl (String)
- SessionToken (GUID)
- 1 Output Parameter
- SessionResponse (string)
Assign it with the ValidateUserSession plugin you created earlier.
Once you are done with PRT, you will now need to go to Power Pages Management (or Portal Management) and register the API with Marius’ custom page. Please read the CAPTCHA article for more details.
Step 4. Creation of an Intermediary Session Creation page
Create a new page and let’s call it AuthenticateUser.
My HTML code is as follows for this page:
<script>
function getParameterByName(name, url = window.location.href) {
name = name.replace(/[\[\]]/g, '\\$&');
var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, ' '));
}
$(document).ready(function(){
var visitedPage = getParameterByName("VisitedPage");
if(visitedPage != null && visitedPage != "" && visitedPage != "undefined")
{
shell.getTokenDeferred().done(function (s) {
var actionInputs = JSON.stringify({
"ContactId" : "{{ user.id }}",
"PageUrl" : visitedPage
});
$.ajax({
url: "/_api/mwo_powerpagesactions?$filter=mwo_operation eq 'CreateUserSessionAPI' and mwo_inputs eq '" + actionInputs + "'",
type: 'GET', // Specifies the HTTP method as POST
headers: {
'Content-Type': 'application/json',
'__RequestVerificationToken': s,
'Authorization': 'Bearer ' + s
},
success: function(response) {
if(response != null && response.value != null && response.value.length > 0)
{
var responseValue = response.value[0];
if(responseValue.mwo_outputs != null && responseValue.mwo_outputs != "" && responseValue.mwo_outputs != "undefined")
{
var responseValueObj = JSON.parse(responseValue.mwo_outputs);
if(responseValueObj != null && responseValueObj.SessionToken != null)
{
location.href = '/signin?ReturnUrl=' + responseValueObj.SessionToken;
}
}
}
},
error: function(xhr, status, error) {
$('#dialogPleaseWait').dialog("close");
console.error('Error:', error);
}
});
});
}
});
</script>
<div class="entity-grid entitylist" data-grid-class="table-striped">
Please wait...
</div>
<div class="row sectionBlockLayout text-left" style="min-height: auto; padding: 8px;">
<div class="container" style="display: flex; flex-wrap: wrap;">
<div class="col-md-12 columnBlockLayout" style="padding: 16px; margin: 60px 0px; min-height: 200px;"></div>
</div>
</div>
Basically, this page is only an intermediary page which will call the CreateUserSession API. This page will generate the session token then redirect the user to /signin page with the ReturnUrl of the destination page the user is visiting. The ReturnUrl will be something like /profile?SessionToken=xxxx.
We will validate the session token within the destination page itself.
Once you have finished creating the intermediary page, you will need to point to this page instead of the direct destination page. For example, instead of pointing to /profile, point the link to /AuthenticateUser?VisitedPage=/profile.
Step 5. Securing the destination page to redirect back to the Intermediary Session Creation Page
Finally, we now need to secure the destination page itself. In this example, we would like to secure the Edit Profile page. So, in Power Pages Management (or Portal Management), edit the Profile web page > Localized Content.
Then, add the following Javascript:
var pageUrl = "/profile";
var sessionToken = getParameterByName("SessionToken");
shell.getTokenDeferred().done(function (s) {
var actionInputs = JSON.stringify({
"ContactId" : "{{ user.id }}",
"PageUrl" : pageUrl,
"SessionToken" : sessionToken
});
$.ajax({
url: "/_api/mwo_powerpagesactions?$filter=mwo_operation eq 'ValidateUserSessionAPI' and mwo_inputs eq '" + actionInputs + "'",
type: 'GET', // Specifies the HTTP method as POST
headers: {
'Content-Type': 'application/json',
'__RequestVerificationToken': s,
'Authorization': 'Bearer ' + s
},
success: function(response) {
var loadPage = false;
if(confirmationMessage.length)
loadPage = true;
else
{
console.log(response);
if(response != null && response.value != null && response.value.length > 0)
{
var responseValue = response.value[0];
if(responseValue.mwo_outputs != null && responseValue.mwo_outputs != "" && responseValue.mwo_outputs != "undefined")
{
var responseValueObj = JSON.parse(responseValue.mwo_outputs);
if(responseValueObj != null && responseValueObj.SessionResponse != null)
{
var sessionResponse = responseValueObj.SessionResponse;
if(sessionResponse == "success")
{
loadPage = true;
}
}
}
}
}
if(!loadPage)
{
location.href = "/AuthenticateUser?VisitedPage=" + pageUrl;
}
else
{
// Session Authentication is successful
}
},
error: function(xhr, status, error) {
console.error('Error:', error);
}
});
}); The code above will execute the ValidateSession plugin we deployed earlier, and if it receives the response “success” string, then session token is valid and user can continue visiting the page. Otherwise, user will be redirected back to the intermediary page, which subsequently will redirect user to the login page.
Conclusion
This approach, albeit more complex and requiring more technical configuration, is actually more versatile than the approach mentioned in the 1st article. This shall work with any identity providers.