HealtheLife Framework SDK
The HealtheLife Framework software development kit (SDK) enables third-party developers to integrate web-based applications, called pagelets, into the HealtheLife patient portal. The HealtheLife Framework SDK includes the tools required to provide a secure and seamless experience for users of the HealtheLife patient portal.
Important! By developing and implementing web applications for use in the HealtheLife framework, organizations agree to adopt any and all responsibility for properly protecting personal health information (PHI) and IDs as outlined in Health Insurance Portability and Accountability Act (HIPAA) compliance rules when using any sort of analytics or tracking applications such as Google Analytics. Cerner cannot log HIPAA audit events for content embedding using the HealtheLife Framework SDK.
Developing a Pagelet
A pagelet is a web application that is designed to be embedded in an inline frame in the HealtheLife patient portal. Inline frames may add overhead, but they also provide the following features:
- CSS and JavaScript Sandboxing: Styling and scripts dependencies do not conflict across pagelets.
- Security: The same-origin policy prevents pagelets from accessing sensitive information in other pagelets.
- Distributed Development: Each pagelet can be released independently of HealtheLife and other pagelets.
- Implementation: Implementing an existing web application as a pagelet requires a low amount of effort.
Browser Context Session Tokens
The BCS token allows non-Cerner browsing contexts to determine a user’s identity using a verifiable token. The BCS token is a time-bound, signed JSON Web Token (JWT), served by the Cerner authorization server. See Browsing Contexts on the Web Hypertext Application Technology Working Group (WHATWG) website for more information.
In commercial production regions, the HealtheLife framework sends browser context session (BCS) tokens in a query parameter in the URL when the pagelets are loaded in an iframe.
Example of BCS Token in URL:
<iframe src="https://my-app.com/pagelet?bcs_token= eyJraWQiOiIyMDE5LTA1LTIyVDIwOjQwOjM4Ljc4MS5lYyIsInR5cCI6IkpXVCIsImFsZyI6IkVTMjU2In0.eyJzdWIiOiJ1cm46Y2VybmVyOmlkZW50aXR5LWZlZGVyYXRpb246cmVhbG06NDlmMTc2NTktODkxYy00MmM5LWFlMjYtZDZhZTc4MjZmNjJkOnByaW5jaXBhbDp0dzVFUDhVRDh3VSIsImF1ZCI6Imh0dHBzOlwvXC9zcHJlcy5wYXRpZW50cG9ydGFsLmhlYWx0aGVpbnRlbnQuY29tIiwiaXNzIjoiaHR0cHM6XC9cL3BsYXlncm91bmQuY29uc3VtZXJwb3J0YWwuaGVhbHRoZWludGVudC5jb20iLCJleHAiOjE1NTg3MDkyMDAsImlhdCI6MTU1ODcwODYwMCwic2lkIjoiNGIwZWRjMmEtMjU5Yi00ODI0LTgyYzYtMTc3MjM4Y2JlZjRjIn0.dDNtHwPpPc2FUmGX_mpWJAA95f1edPttN-6m6uin6dM-P4xRma1yFNNhuBFK9JE-YT39clSPaEjHXnSh6EMnew " scrolling="no" title="Relationships" allow="geolocation *; microphone *; camera *; geolocation; microphone; camera;" style="overflow: hidden; height: 269px;"></iframe>
In the federal region, the HealtheLife framework omits the token from being passed to the URL, so third-party applications must retrieve the BCS token through a method provided by the SDK, getBCSToken()
. See this section of the SDK documentation for more about this method.
Token Fields
Example Decoded BCS Token
{
"kid": " 2019-05-22T20:40:38.781.ec ",
"typ": "JWT",
"alg": "ES256"
}
{
"sub": " urn:cerner:identity-federation:realm:49f17659-891c-42c9-ae26-d6ae7826f62d:principal:tw5EP8UD8wU ",
"aud": " https://spres.patientportal.healtheintent.com",
"iss": " https://playground.consumerportal.healtheintent.com",
"exp": 1558709200,
"iat": 1558708600,
"sid": " 4b0edc2a-259b-4824-82c6-177238cbef4c "
}
When decoded, the content of the token includes a header and payload, like the example to the right. The fields in the decoded token enable applications to identify and authorize users. The following fields are included in the header and are used to verify the token signature:
Name | Description |
---|---|
alg | The cryptographic algorithm used to sign the BCS token. |
kid | The ID of the key used to sign the BCS token. |
typ | The media type of the BCS token. |
The payload fields, or token claims, below are used to identify the user, set the content security policy of the application, and verify that the token has not expired. See Registered Claim Names on the Internet Engineering Task Force (IETF) website for more information about token claims.
Name | Description |
---|---|
aud | The origin domain of the browsing context that receives the token. |
exp | The expiration time on or after which the BCS token must not be accepted for processing, formatted as a UNIX epoch. |
iat | The time at which the BCS token was issued, formatted as a UNIX epoch. |
iss | The root URL of the website, or HealtheLife framework instance, that acts as a container for the pagelet. |
sid | The proprietary Cerner ID associated with the session. This ID can be used in HIPAA auditing and other security logging. |
sub | A principal URI that identifies the user. |
Retrieving a Token
The SDK provides a method that returns the current user’s BCS token as a Javascript promise. This method should be used in environments in which the BCS token is not passed to the pagelet URL.
Example Token Retrieval
$HL.App.getBCSToken().then((token) => {
this.bcsToken = token;
});
Validating a Token
Example Token Validation:
require 'net/http'
require 'json'
require 'json/jwt'
JWK_URI = 'https://authorization.cerner.com/jwk'
def decode_token(token)
keys = Net::HTTP.get(URI(JWK_URI))
jwk_set = JSON::JWK::Set.new(JSON.parse(keys))
return decoded_token(JSON::JWT.decode token, jwk_set)
end
Use an open-source library to validate tokens to ensure that they are signed and not tampered with. As a minimum requirement, the library must support validation for Elliptic Curve Digital Signature Algorithm (ECDSA) using a P-256 curve and SHA-256 hash algorithm. Tokens can be validated using a JSON web key, which can be obtained at the following URL: https://authorization.cerner.com/jwk.
See the JSON Web Tokens website for a list of available open-source libraries, and see JSON Web Key on the IETF website for more information about JSON web keys.
Keeping a Token Up to Date
Example Token Validation:
<script>
$HL.App.init({
acls: [*]
});
$HL.App.on('hl.sdk.refreshToken', (detail) => {
token = detail.bcsToken;
});
</script>
The hl.sdk.refreshToken event listener sends updated tokens to the consuming application. To ensure that the token does not time out during the user’s browsing session, the hl.sdk.refreshToken event listener must be monitored and the updated token state must be maintained.
Serving Pagelet Requests
Example Server:
require 'sinatra'
require 'net/http'
require 'json'
require 'json/jwt'
# Keys must be cached.
keys = Net::HTTP.get(URI('https://authorization.cerner.com/jwk'))
jwk_set = JSON::JWK::Set.new(JSON.parse(keys))
get '/index.html' do
bcs_token = params[:bcs_token]
locale = params[:locale]
begin
decoded_token = JSON::JWT.decode bcs_token, jwk_set
rescue
return 403
end
# Ensure that the token is not expired and that the token was created for this host.
if !(Time.now.between?(Time.at(decoded_token[:iat]), Time.at(decoded_token[:exp])) &&
request.host == decoded_token[:aud])
return 403
end
headers['Cache-Control'] = 'no-cache'
headers['Content-Type'] = 'text/html; charset=utf-8'
# Allow this pagelet to be embedded as an iframe only on the issuer's domain.
headers['X-Frame-Options'] = "allow-from #{decoded_token[:iss]}/"
headers['Content-Security-Policy'] = "frame-ancestors #{decoded_token[:iss]}/;"
%Q(
<html lang='{locale}' hidden>
<head>
<title>My Pagelet</title>
</head>
<body>
Hello World
<script src="https://healthelife.healtheintent.com/healthelife_sdk.js"></script>
<script>
$HL.App.init({
acls: ["#{decoded_token[:iss]}"]
});
</script>
</body>
</html>
)
end
A pagelet is simply an HTML page that is embedded in the HealtheLife framework. However, to securely embed a pagelet, a server that handles pagelet requests must perform additional steps to ensure that the pagelet works in the framework and has a proper security policy. To the right is a simple server written in Ruby that serves pagelet requests.
Verifying the BCS Token Claims
Example Claims Verification:
# Ensure that the token is not expired and that the token was created for this host.
if !(Time.now.between?(Time.at(decoded_token[:iat]), Time.at(decoded_token[:exp])) &&
request.host == decoded_token[:aud])
return 403
end
Verify the following claims on BCS tokens before returning a successful response:
- iat and exp: Verify that the current time occurs between the issued and expiration times on the token.
- aud: Verify that the token was created for the host in the request. This action prevents tokens from being repurposed for other sites.
Setting a Content Security Policy
Example Content Security Header:
# Allow this pagelet to be embedded as an iframe only on the issuer's domain.
headers['Content-Security-Policy'] = "frame-ancestors #{decoded_token[:iss]}/;"
# Set X-Frame-Options for legacy browser support.
headers['X-Frame-Options'] = "allow-from #{decoded_token[:iss]}/"
Framed applications need to set additional security policies to ensure that they are not vulnerable to UI redress attacks. A content security policy header must be sent in the response header to ensure that the pagelet can be framed only in the context of the issuer site. See Clickjacking on the Open Web Application Security Project (OWASP) website for more information about UI redress attacks.
Setting the Locale
locale = params[:locale]
The locale used by the framework is sent to each pagelet in the locale
query parameter. Pagelets need to recognize this locale if possible.
Pagelet HTML
<html lang='en-us' hidden>
<head>
<title>My Pagelet</title>
<style>
html[hidden] {
display: none;
visibility: hidden;
}
</style>
</head>
<body>
Hello World
<script src="https://healthelife.healtheintent.com/healthelife_sdk.js"></script>
<script>
$HL.App.init({
acls: ["#{decoded_token[:iss]}"]
});
</script>
</body>
</html>
The SDK is initialized by calling $HL.App.init
and specifying the access-control list (ACL) in the acls
property as equal to the token issuer claim. The ACL ensures that any browser that does not support the Content-Security-Policy or X-Frame-Options directive still hides the pagelet using the hidden attribute on the HTML element. When the origin of the parent site is verified to equal the token issuer, the SDK unhides the pagelet.
HealtheLife Framework JavaScript SDK
A production version of the HealtheLife Framework JavaScript SDK is minified and optimized at the following content delivery network (CDN) location: https://healthelife.healtheintent.com/healthelife_sdk.js
Browser Support
The HealtheLife Framework JavaScript SDK supports the following browsers:
- Apple Safari
- Google Chrome
- Microsoft Edge
- Microsoft Internet Explorer 11.0 and later
- Mozilla Firefox
Framework JavasScript API
The API methods are in the $HL.App
namespace.
$HL.App.init(options)
$HL.App.init Example:
$HL.App.init({
acls: ["https://yourdomain.com"],
activePatientChangeHandler: (activePatient) => {
// TODO: Update App to reflect active patient
return true;
},
onReady: function() {
console.log('Pagelet initialized!')
},
targetSelectors: '.modal'
});
Initializes the pagelet. Call $HL.App.init
as soon as possible to connect the pagelet to the framework. Initialization uses the following process:
- Verify that the pagelet is being framed in a trusted property that is identified by the
acls
property. - Resize the iframe to fit the height of the content in the pagelet to eliminate vertical scrolling in the iframe.
Note: Initialization must be placed at the end of the body of a pagelet to prevent issues where resources and variables in the pagelet body do not return a null value.
The following properties can be set in the options object:
Property | Type | Default | Description |
---|---|---|---|
acls | String[] | [] | The list of trusted origins that are authorized to be embedded in the pagelet. |
onReady | function | null | A callback when initialization is complete. |
targetSelectors | String | null | A CSS selector that identifies floated or absolutely positioned (fixed) elements to be included in the iframe sizing calculation. |
activePatientChangeHandler | function | null | A handler that is passed an object indicating the patient identifier type and value. This is a function used to handle patient change events in the framework and from other apps embedded in the framework. The handler returns a Boolean value of either true, indicating the pagelet is managing the patient context change and will update the UI, or false, indicating the pagelet is not managing the patient context change and must be reloaded by the framework. |
$HL.App.scrollIntoView()
$HL.App.scrollIntoView Example:
$HL.App.scrollIntoView()
Instructs the parent browser window to scroll until the pagelet is in view.
$HL.App.routeTo(options)
// See the following example that uses the path property to navigate to another page in the framework.
$HL.App.routeTo({
path: '/pages/messaging'
});
// A better approach is to use the alias property to route to a page using its alias.
// Aliases are assigned to the configured pages in the HealtheLife framework.
// This ensures that if the path to the page changes, the URL does not break.
$HL.App.routeTo({
alias: 'messaging'
});
// See the following example that routes using the context property:
$HL.App.routeTo({
alias: 'messages.show',
context: {
id: '3298a9adfa98dsa',
}
});
Navigates the framework to the given path. The following properties can be set in the options object:
Property | Type | Default | Description |
---|---|---|---|
path | String[] | [] | The parent application’s path to which to navigate. The path value is used by default if both the alias and path properties are specified. |
alias | function | null | An alias that corresponds to a path in the parent application. Aliases for configured pages can be provided by Cerner. |
context | Object | null | Extra, contextual information to send to pagelets on the page. The context property is sent to pagelets in query parameters at the end of the pagelet URL. The pagelets that receive the context determine how it is used. Note: The context is not applied to pagelets if a bookmark exists (see the bookmarkLink property below). |
bookmarkLink | String | null | The URL loaded into the pagelet on the page that is accessed instead of the original pagelet URL defined for the page. Note: The URL of the original pagelet configured by the parent application and the bookmarkLink URL must have the same origin. For example, if pagelet A is mounted in the iframe on page one and has a button that triggers routeTo with a path of /pagetwo and a bookmarkLink equal to pagelet B’s URL, the parent application opens page two and pagelet B is displayed in the iframe even though page two was originally configured to display pagelet C. This bookmarkLink property is powerful for users because it gives them the ability to bookmark the page in that specific state, view it later with pagelet B displayed in the iframe, and send a link to the page in that specific state to another user. |
$HL.App.openExternalURL(url)
$HL.App.openExternalURL(url) Example:
$HL.App.openExternalURL('https://docs.healtheintent.com');
Opens the given URL outside of the framework. This action must only be used for third-party or otherwise non-framework URLs, such as a link to Google Maps or to a health care provider’s scheduling application. For framework URLs, use $HL.App.routeTo(options)
.
Property | Type | Default | Description |
---|---|---|---|
url | String | null | The URL to be opened. |
$HL.App.openModal(options)
$HL.App.openModal(options) Example:
$HL.App.openModal({
url: 'https://docs.healtheintent.com/examples/basic_authorization_pagelet/',
});
Opens the pagelet URL in a modal. The following properties can be set in the options object:
Property | Type | Default | Description |
---|---|---|---|
url | String | null | The URL of the pagelet to open in the modal dialog box. |
title | String | null | The title for the modal dialog box. |
$HL.App.closeModal()
$HL.App.closeModal() Example:
$HL.App.closeModal();
Closes the modal dialog box that was opened using the openModal method.
$HL.App.setPersonContext(millenniumPersonId)
$HL.App.setPersonContext(millenniumPersonId) Example:
$HL.App.setPersonContext('123');
Sets the millenniumPersonId
value in the global context and emits an event so that all pagelets in the framework are synchronized by specifying the millenniumPersonId
as a query parameter in the pagelet URL. The pagelets that receive the millenniumPersonId
value determine which person is in use and whether the user has access to view the data for that person.
Property | Type | Default | Description |
---|---|---|---|
millenniumPersonId | String | null | The Cerner Millennium person ID to be set in the context and specified in the URL for the pagelets. |
hl.sdk.refreshToken()
hl.sdk.refreshToken() Example:
$HL.App.on('hl.sdk.refreshToken', (detail) => {
token = detail.bcsToken;
});
Refreshes the token for the requested pagelet. When tokens are used, events must be monitored to ensure that tokens do not preemptively expire. See the Keeping a Token Up to Date section for more information.
$HL.App.getBCSToken()
Returns a promise that resolves to the current user’s BCS token.
$HL.App.getBCSToken() Example
$HL.App.getBCSToken().then((token) => {
this.bcsToken = token;
});
$HL.App.getActivePatient
Returns a Promise that resolves to an object identifying the current active patient. Can be
invoked after the SDK is initialized and the onReady
callback is fired.
HL.App.getActivePatient() Example
$HL.App.init({
acls: ["*"],
activePatientChangeHandler: (activePatient) => {
// TODO: Update App to reflect active patient change events from other apps
return true;
},
onReady: () => {
const activePatientPromise = $HL.App.getActivePatient();
activePatientPromise.then(activePatient => {
// TODO: Update App to reflect active patient
console.log('Active Patient Type:', activePatient.type);
console.log('Active Patient Value:', activePatient.value);
});
activePatientPromise.catch(err => {
console.error('An unexpected error occurred while retrieving the active patient.', err);
});
}
});
$HL.App.setActivePatient
Sets the active patient to be used in the framework for all applications. UI updates to the active
patient change need to occur in the activePatientChangeHandler
to ensure that it is successfully
set on the framework.
HL.App.setActivePatient(patient) Example:
$HL.App.init({
acls: ["*"],
activePatientChangeHandler: (activePatient) => {
// TODO: Update App to reflect active patient
console.log('Active Patient Type:', activePatient.type);
console.log('Active Patient Value:', activePatient.value);
return true;
},
onReady() {
// Set the framework to Patient Abc123 immediately when the app loads.
$HL.App.setActivePatient({
type: 'MRN'
value: 'abc123'
});
}
});
Styling Considerations
Pagelets embedded in the framework require the special styling considerations below.
Sizing
The SDK determines the height of the iframe using the offset height of the document body. It then monitors the Document Object Model (DOM) and updates the height when changes are made to the HTML. This means that the pagelet cannot set the <html>
and <body>
elements to a relative height that is determined by the viewport, or, in the case of a pagelet, the iframe. For example, a height of either 100% or 100vh breaks the sizing calculation. See the following recommendations:
Recommended | Not Recommended |
---|---|
html, body { height: auto; } or html, body { height: 200px; } |
html, body { height: 100%; } or html, body { height: 100vh; } |
Fluid and Responsive Design
Pagelets in the framework can be configured to be any size and displayed on anything from large screens to small mobile devices. The styles for pagelets need to be fluid and responsive to ensure that a pagelet looks acceptable at every screen size. This is especially important because the width of the iframe is used to determine the media query in CSS.
Recommended | Not Recommended |
---|---|
Allow content to fill 100 percent of the available screen width, and use media queries to change the style when necessary. | Do not fix content to a specific width or assume an ideal viewport size. |
Out of Flow Elements
Floated and absolutely positioned elements are taken out of normal flow and not included in the pagelet height calculation. Depending on where the UI is displayed, this may cause the following problems:
- Buttons positioned to be fixed to the bottom of the screen may be cut off.
- A menu may be hidden when the pagelet is opened.
- A dialog box may be cut off.
- An entire positioned, fixed UI may not be sized correctly.
When possible, avoid position-fixed elements. If they are required in the UI, use the targetSelectors
property to identify those UI elements so they are included in the resizing calculation for the $HL.App.init()
method.
Background Colors
Pagelets must have their background color to transparent to ensure that they seamlessly blend with the background color on the HealtheLife framework. Because the background color can be themed, a specific color cannot be guaranteed. Content that needs to be on a white or light background must be placed in a card. Cards and other containers must have no spacing between their borders and the iframe to ensure that they align with other content in the framework.
Recommended | Not Recommended |
---|---|
Place content in cards to ensure it is readable with a consistent contrast ratio, and ensure that content is flush with the iframe border. | Do not set a background color on your pagelet’s html or body element, and do not set a padding or margin on the body or container elements. |
Integrating with Platform APIs
Getting Started
A system account is required to use Oracle Health Data Intelligence APIs, and the system account must be authorized to use the APIs that you use for your pagelet. See Getting Started for more information.
Retrieving the Consumer
Example GET Request to Retrieve a Consumer
GET /consumers?aliasType=USER&aliasSystem=49f17659-891c-42c9-ae26-d6ae7826f62d&aliasValue=tw5EP8UD8wU
The Health Data Intelligence consumer can be retrieved by sending a GET
request to the /consumers
endpoint of the Health Data Intelligence Consumer API and specifying the following query parameters:
- aliasType: Specify the USER alias type.
- aliasSystem: Specify the identity realm from the BCS token subject as the alias system.
- aliasValue: Specify the principal from the BCS token subject as the alias value.
The user’s identity realm and principal can be found in the principal URI that is populated in the BCS token sub
property. For example, if the BCS token subject is urn:cerner:identity-federation:realm:**49f17659-891c-42c9-ae26-d6ae7826f62d**:principal:**tw5EP8UD8wU**
, the user could be retrieved by specifying the query parameters above as follows:
- aliasType: USER
- aliasSystem: 49f17659-891c-42c9-ae26-d6ae7826f62d
- aliasValue: tw5EP8UD8wU
Displaying the Consumer Information in a Pagelet
Example Pagelet That Retrieves a Health Data Intelligence Consumer
require 'sinatra'
require 'net/http'
require 'json'
require 'json/jwt'
# Keys should be cached.
keys = Net::HTTP.get(URI('https://authorization.cerner.com/jwk'))
jwk_set = JSON::JWK::Set.new(JSON.parse(keys))
get '/index.html' do
bcs_token = params[:bcs_token]
begin
decoded_token = JSON::JWT.decode bcs_token, jwk_set
rescue
return 403
end
# Ensure that the token is not expired and was created for this host.
if !(Time.now.between?(Time.at(decoded_token[:iat]), Time.at(decoded_token[:exp])) &&
request.host == decoded_token[:aud])
return 403
end
subject = decoded_token[:sub]
principal_uri = subject.match(/realm:(?<realm>[\w+-]+):principal:(?<principal>\w+)/)
# API changes based on tenant.
consumer_uri = URI("https://playground.api.us.healtheintent.com/consumer/v1/consumers")
consumer_uri.query = URI.encode_www_form({
aliasType: 'USER',
aliasValue: principal_uri[:principal]
aliasSystem: principal_uri[:realm]
})
http = Net::HTTP.new(consumer_uri.host, consumer_uri.port)
http.use_ssl = true
consumer_response = http.get(consumer_uri.request_uri, {
'Authorization': ENV['BEARER_TOKEN'],
'Content-Type': 'application/json'
})
consumers = JSON.parse(consumer_response.body)["items"]
consumer = consumers.first
headers['Cache-Control'] = 'no-cache'
headers['Content-Type'] = 'text/html; charset=utf-8'
# Allow this pagelet to be embedded as an iframe only on the issuer's domain.
headers['Content-Security-Policy'] = "frame-ancestors #{decoded_token[:iss]}/;"
# Set the X-Frame-Options for legacy browser support.
headers['X-Frame-Options'] = "allow-from #{decoded_token[:iss]}/"
%Q(
<html lang='en-us' hidden>
<head>
<title>My Pagelet</title>
<style>
[hidden] {
display: none;
}
</style>
</head>
<body>
Hello #{consumer["name"]["formatted"]}
<script src="https://healthelife.healtheintent.com/healthelife_sdk.js"></script>
<script>
$HL.App.init({
acls: ["#{decoded_token[:iss]}"]
});
</script>
</body>
</html>
)
end
After you retrieve the consumer, you can use it to retrieve and display demographic information or to identify any patients that are linked to the consumer. See Consumer API v1 for more information about using the Health Data Intelligence Consumer API.
Note: Consumer demographic information may be different from the demographic information of patients in the electronic health record (EHR). Patients need to be identified using an MRN.
Additional Resources
Health Data Intelligence Resources
External Resources
- Browsing Contexts on the WHATWG website
- Clickjacking on the Open Web Application Security Project (OWASP) website
- JSON Web Key on the IETF website
- JSON Web Tokens
- Registered Claim Names on the IETF website