I’ve recently been working with a customer to consolidate multiple self-service portals onto the ServiceNow Service Portal. As part of this, we embedded external web content into the ServiceNow portal using an iFrame.
Our customer was dealing with a myriad of different and inconsistent self-service portals provided by their business applications. This made it confusing and difficult for users to find information and request services. For example, each department had their own tool and service portal so users often didn’t know which portal to go to get HR information or where to submit a Facilities, Training or IT request.
The preferred solution was to migrate these applications or surface their data in ServiceNow via an integration. However, for some applications this was not an option in the near or long term. So, the solution was to embed the application directly into the ServiceNow Service Portal using iFrames.
iFrames allow a web page to insert content from another source. Once very popular, today they are usually discouraged due to security, usability and SEO issues and are often seen as a deprecated and legacy element of the old web. However, they are part of the HTML5 standard and while those issues are real, they can be mitigated or simply do not apply in some cases. As a developer and architect, my job is to select the most appropriate tools to achieve the best outcome, and in this case, iFrames fit the bill. However, that’s not saying we had an easy time.
Creating a custom widget
Successful service portal projects always involve mock-ups of the homepages and storyboards for the navigation. This makes it very easy to chop and change the look and feel during workshops and allows contributors to provide feedback. From our sessions with the customer, the need to embed content of different sizes and from multiple systems on the same page was uncovered. We need to show content full screen with just the Service Portal header and breadcrumbs surrounding the embedded page, and we needed to include video that could be resized and maximised.
Before we get to the code, I want to cover some of the design decisions and issues we ran into. If you want to grab the Update set to see it working in your own instance, you can download the files here and here
Designing for reusability
Wherever you can, you want to avoid writing the same code twice. You also want to allow changes to be made without requiring code to be written. So, rather than cloning the widget and making small adjustments for different use cases, we wanted one widget that could handle most situations. To achieve this, we created a custom widget that was configurable via widget instance options and URL parameters. This allowed each instance of our widget to behave differently and changes in design could be made easily via the Page Designer UI or adding parameters to a hyperlink.
Only four options were needed to satisfy the scenarios from the mock-ups.
- URL – The URL of the external page to embed
- Size – A list of available sizes and aspect ratio. We landed on large, medium, small, and video. Doing this meant we could hide the not so obvious CSS directives required to best show the content. We did attempt to adjust the size based on the content inside the frame but ran into problems. More on those later.
- Label – Descriptive text that was used to support assistive readers and update the baseline ServiceNow breadcrumb widget if it was present on the same page.
- Set Page Title – A checkbox to override the page title with the Label field. This was helpful when the iFrame was maximised to fill the page, with just the Service Portal header and breadcrumbs visible. This was a view that was needed often, and we did not want a generic page title each time we did this.
Figure 1 - The widget instance options shown in Page Designer
After creating the instance options in the Widget Editor, the options are then accessed in the client script using the following example. Here, we also add the $location service to the controller to gain access to the url parameters. In the following example, the URL parameter, if present, will override the widget option.
api.controller = function ($location) {
var c = this;
var params = $location.search();
var url = params.url || c.options.url;
var size = params.size || c.options.size;
var label = params.label || c.options.label;
var setPageTitle = params.set_page_title || c.options.set_page_title;
};
Setting the size with Style
Ideally, we wanted to dynamically adjust the size of the iFrame according to the content that it contained. However, the browser will not allow a parent page to communicate with an embedded page unless that page allows it. The default policy is to restrict access and most sites leave it as that. These policies are known as cross origin resource sharing (CORS).
Many sites do not allow you to modify the CORS policies so a neat trick, if you can add JavaScript to your site, is to utilise the window object to send a message from the embedded site to the parent site. I learnt this from Dylan Lindgren’s excellent article. Here is a simple example using pure JavaScript.
From the embedded site, get the size of the content you want to show and then send it in an event to the parent.
<div id="container">
<p>Some content to be embedded on the page.</p>
</div>
<script>
var content = document.getElementById("container");
var size = {
width: content.offsetWidth,
height: content.offsetHeight
};
window.parent.postMessage(size, "*");
</script>
From the parent site, catch the event and resize the iFrame element.
<iframe id="my_iframe"></iframe>
<script>
window.addEventListener("message", receiveMessage, false);
function receiveMessage(event) {
var size = event.data;
var myiFrame = document.getElementById("my_iframe");
myiFrame.style.height = size.height + "px";
myiFrame.style.width = size.width + "px";
}
</script>
However, we could not use this technique since the sites we were embedding did not allow custom JavaScript to be added to their pages. So, in the end we abandoned the idea of dynamically adjusting the size and instead settled on predefining size profiles in SCSS that could be chosen from a list of; large, medium, small & video. These included fixed width / height options and a fixed ratio option for showing resizable video.
.external-content {
overflow: hidden;
position: relative;
}
.external-content-large {
height: 3000px;
}
.external-content-medium {
height: 0; padding-bottom: 75%;
}
.external-content-small {
height: 150px;
width: 200px;
}
.external-content-video {
height: 0;
padding-bottom: 56.25%; // 16:9 aspect ratio
padding-top: 35px;
}
.external-content iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
}
Figure 2 - Widget style sheet
In the end, this gave us more control over the content and prevented embedded pages from breaking the layout and design of the parent.
Sandboxing the iFrame
It is sound advice to only grant the minimum permissions needed to get the job done. The principle of least privilege can be applied to the iFrame to enforce all restrictions and then selectively enabling only those capabilities that are required. The sandbox attribute will do just this. By adding the sandbox attribute to the element, all restrictions are enforced. Unless the content is static html, this will be too restrictive for the web page to function. The sandbox attribute can be given a whitelist of features to enable. For the sites we were embedding we needed the following features enabled.
allow-same-origin
– required to allow the frame to access cookies and data coming from the same origin as the embedded page.
allow-forms
– allows the frame to submit forms. This was required since most of the content we had to embed was a form to request a service.
allow-modals
– allows dialog boxes such as Alert, Confirm and Print
allow-popups
– allow the frame to create popup windows.
allow-scripts
– Allow JavaScript to run in the frame. All the content we had used JavaScript so this was required.
To show full screen video, the attribute allow='fullscreen' was required on the iframe element. In addition some error handling code is added for when a URL is not provided and if the browser does not support iFrames. Again, good practice here is not to fail silently and provide some useful feedback in the event something goes wrong.
Altogether, the html template for the widget is shown below.
<div ng-if="showError" class="padding">
<div class="alert alert-warning" role="alert">
Provide a URL as a widget option or as a parameter, 'url'
</div>
</div>
<div class="external-content" ng-class="contentClass">
<iframe
title="{{label}}"
aria-label="{{label}}"
ng-src="{{frameSource}}"
frameborder="0"
scrolling="yes"
sandbox="allow-same-origin allow-forms allow-modals allow-popups allow-scripts"
allow="fullscreen"
>${Browser does not allow iFrames}
</iframe>
</div>
</div>
Figure 3 - Widget HTML Template
Setting the Frame source
The content in the frame is retrieved from the URL found on the src attribute. To set the content, title, and label differently for each instance, AngularJS was used to assign the widget options to these attributes. However, AngularJS will not allow you to set the src attribute to a URL unless you explicitly trust the value to be a safe URL.
To do this the Strict Contextual Escaping ($sce) service needs to be added to your controller
api.controller = function ($scope, $location, $sce) {
and then used to trust the value as a URL..
$scope.frameSource = $sce.trustAsResourceUrl(url);
Making it seamless, almost..
One of the most frequent scenarios was to show embedded content taking the full width of the portal with just the Service Portal header and breadcrumbs visible. Every instance of this used one page, with the page title and breadcrumbs updated according to the options provided in the URL.
If the breadcrumbs widget is present, then we can emit an object with the label event to the $rootScope
where the breadcrumbs widget will listen and update itself accordingly.
$rootScope.$emit('sp.update.breadcrumbs', [{label:"Show This"}]);
To update the page title, we use jQuery to select the title element and update the text.
$('head title').text("My new title");
Putting this all together, the widget client script looked like this;
var c = this;
var params = $location.search();
var url = params.url || c.options.url;
var size = params.size || c.options.size;
var label = params.label || c.options.label;
var setPageTitle = params.set_page_title || c.options.set_page_title;
if (!url) $scope.showError = true;
else {
$scope.frameSource = $sce.trustAsResourceUrl(url);
$scope.contentClass = "external-content-" + size;
$scope.label = label;
}
$rootScope.$emit('sp.update.breadcrumbs', [{label:label}]);
if(setPageTitle)
$('head title').text(label);
};
Figure 4 - Widget Client script
Handling CORS Security policies
Now that the widget is built, we need to look at each of the external sources and confirm they allow the content to be embedded. This is controlled by CORS (cross origin resource sharing) policies sent in the HTTP response headers by the external source’s web server. Using your browser’s developer tools (F12 in Chrome), you can view the headers attached to each request and response made between your browser and the web server. There are several to look out for. The ones we encountered where;
x-frame-options – Many of the sites we wanted to embed had this header present. It has two directives as options, DENY or SAMEORIGIN and neither allow the page to be embedded in an iFrame. We had to speak to the owners of these applications to have this header removed. However, with it removed, it meant that any site could now embed the content which raises concerns around types of malicious attacks such as click hijacking. So, this led us to recommending they send the next header as a solution.
Control-Security-Policy – This header allows us to declare policies to prevent the type of attacks that are a concern when content is embedded. Specifically, we used the directive frame-ancestors to declare specific domains that are allowed to embed the content. For example, to allow any ServiceNow instance to embed the content you would send the following.
Content-Security-Policy: frame-ancestors ‘self’ https://*.service-now.com
Incidentally, since the Quebec release, it is now possible to modify HTTP response headers sent from your ServiceNow instance. Useful if roles are reversed and you need to embed ServiceNow content into another site.
Authentication
Critical to a seamless experience for the user was ensuring they were not presented with a login prompt within the iFrame. Most organisations will have enabled a common identity provider across their applications. But we also needed to ensure that single sign-on was enabled, meaning that once authenticated, the user isn’t asked again for their password from other applications.
This was one of the first things to test as it was critical to the success of our primary goal of providing a seamless experience for the end user.
Final thoughts
We achieved our original goal of providing a single site for users to get information and request services. We were able to accommodate systems that could not integrate or migrate by embedding them into the ServiceNow portal using a simple and reusable widget. We have received great feedback from the users and some of the other application owners are now open to ideas of migrating or at least integrating directly with the platform.
The code
You can download an update set containing the widget here and here. I have also exported the Service Portal page that was used to show the iFrame full screen. Import these into your instance and add the iFrame widget to a Service Portal page. You can also test the External Content page and configuring the widget options from the URL by navigating to;
https://<your instance>.service-now.com/sp?id=external_content&label=Lego%20Channel&size=video&url=https:%2F%2Fwww.youtube.com%2Fembed%2F4aQwT3n2c1Q