Advanced Topics

Content Portal Internationalization

Internationalization

Heretto Deploy Portal is fully internationalized, meaning the portal and its content can be presented in any language/locale combination. To have a portal that supports more than one locale, you need to localize the content in Heretto CCMS and make a few configuration changes.

The portal supports these capabilities:

  • Localization of the portal's interface, by substituting locale-specific text

  • Presentation of localized content

  • The selection of a locale by a portal visitor so that the portal responds to that locale

  • Detection of a user's locale based on the user's preferred language set in the browser

Localize the portal source content

The Heretto Deploy portal can display locales for any content that's been localized in Heretto CCMS.

Heretto CCMS supports easy localization of content.

Review the OpenID Connect documentation for details on localizing content in Heretto CCMS.

Localize Heretto Deploy Portal text strings

The portal interface needs to be localized for each language you want to support.

By default, the portal UI text strings are in English (en). You can customize and localize the text strings.

  1. In the config.json file, locate the i18n statement.
      "i18n": {
            "en": {
                "label.tenant": "eboat",
                "label.section": "All Sections",
                "label.home": "Home",
                "label.sections": "Sections",
                "label.section-contents": "${label.section} ${label.contents}",
                "label.contents": "Contents",
                "label.filters": "Filters",
                "label.related": "Related",
                "label.resources": "Resources",
                "label.articles": "Articles",
                "label.featured": "Featured",
                ...
           }
      }
     
    
  2. For each new locale in the "i18n" statement, perform the following steps.
    1. Copy the entire "en" block, and paste a new entry after the close bracket for "en".
    2. In the new entry, change "en" to the appropriate two-letter code.
      Note:

      Use the two-letter language codes as noted in ISO-639-1.

    3. Replace the new locale's text strings with translations, as appropriate.
  3. Save and close the config.json file.

    When you activate the new locales in the portal deployment, the text strings in the user interface will be in the corresponding locale.

Add locales to a deployment for an internationalized portal

With your content and portal localized, the last step is to create a deployment that supports the configured locales. You can do this by editing the existing deployment associated with your portal.

Set up a deployment to include locales by updating the existing deployment associated with your portal.

  1. In the Heretto CCMS, navigate to Administration > Deployments.
  2. Next to the deployment with one or more locales, click Edit Deployment.
  3. In the Edit Deployment window, check the box next to the locales you want to use in the portal.

    example edit deployment configuration

  4. Optionally, check the Add locale prefix to hrefs if you want the locale reflected in your URIs for the portal pages.
  5. Save your changes.

    When the portal republishes the sitemap, the locales will be available.

Internationalized Strings Configuration Reference

If needed, you can easily modify the default internationalization and localization strings used by your Heretto Deploy Portal. You can use this file to change the text that shows up in your Content Portal, such as the term "Articles".

Default Localization Strings

You can overwrite the default settings by editing the config.json file.

"i18n": {
  "en-US": {
    "label.tenant": "Your_Organization",
    "label.section": "Section",
    "label.home": "Home",
    "label.sections": "Sections",
    "label.section-contents": "${label.section} ${label.contents}",
    "label.contents": "Contents",
    "label.filters": "Filters",
    "label.related": "Related",
    "label.resources": "Resources",
    "label.articles": "Articles",
    "label.featured": "Featured",
    "label.filter": "Filter",
    "label.filter.apply": "Apply ${label.filter}s",
    "label.filter.clear": "Clear ${label.filter}",
    "label.filter.results": "${label.filter} Results",
    "label.result": "result",
    "label.show-contents": "Show ${label.contents}",
    "label.show-sections": "Show Page Sections",
    "label.print": "Print page",
    "label.print-sub": "${label.print} and subpages",
    "label.last-updated": "Last updated: ${date}",
    "label.related-resources": "${label.related} ${label.resources}",
    "label.related-articles": "${label.related} ${label.articles}:",
    "label.search.facet.Information_Type": "Information Type",
    "label.search.facet.Area": "Area",
    "label.search.facet.Mobile": "Mobile",
    "label.featured-articles": "${label.featured} ${label.articles}",
    "label.featured-videos": "${label.featured} Videos",
    "label.whats-new": "What's New",
    "label.version.prefix": "v ",
    "label.version.format": "${title}",
    "message.search.placeholder": "Search ${label.section} Help",
    "message.search.results": "Search Results",
    "message.missing-content": "[MISSING CONTENT]",
    "plural.0": "zero",
    "plural.1": "one",
    "plural.#": "any",
    "http.error.404": "not-found",
    "http.error.#": "other",
    "message.search.results-found": "${message.search.results-found.${@plural}}",
    "message.search.results-found.zero": "No results found",
    "message.search.results-found.one": "One result found",
    "message.search.results-found.any": "${startIndex} - ${endIndex} of ${@count} results found",
    "message.explore": "Explore Topics",
    "message.hero.title": "Welcome to ${label.tenant}'s Help Portal",
    "message.hero.sub-title": "Welcome to ${label.tenant}'s Help Portal",
    "message.error.known-issues-link": "View known issues ",
    "message.error.known-issues": "with the EZD Content Portal",
    "message.error.status": "${message.error.status.${@http.error}}",
    "message.error.next-steps": "${message.error.next-steps.${@http.error}}",
    "message.error.status.not-found": "Sorry, we couldn't find that page.",
    "message.error.status.other": "Sorry! Something seems to have broken on our end. An alert has been sent to the system admin.",
    "message.error.next-steps.not-found": "You can select a topic to open the Help or use the search field.",
    "message.error.next-steps.other": "You can select a topic to open the Help or use the search field.",
    "message.search.result-found": "Search Results",
    "message.browser.not-supported": "You seem to be using an unsupported browser.",
    "message.browser.please-use": "To visit, please use one of the following browsers:",
    "message.browser.links": "Use the links to download the latest version of a supported browser if you don't already have one installed.",
    "message.browser.chrome": "Chrome",
    "message.browser.firefox": "Firefox",
    "message.browser.edge": "Edge",
    "message.browser.safari": "Safari",
    "message.footer.copyright": "Copyright: © 2021 Intellectual Property. All rights reserved.",
    "message.chunked.title": "${content.title}",
    "message.user.welcome": "${given_name}",
    "message.user.login": "Login",
    "message.user.logout": "Logout",
    "message.login": "Please sign in.",
    "message.logout.success": "You have been successfully logged out!",
    "message.logout.login-again": "Sign in again",
    "message.logout.sso-logout": "Sign out of SSO",
    "message.logout.redirect": "This page will redirect in ${delay} seconds...",
    "message.select.version": "${selectedVersion}",
    "message.notification-banner.link-text": "Send us your feedback",
    "message.notification-banner.text": "&nbspon the new site. We appreciate it."
  }
}

Keywords

For advanced localized strings configuration, you can use the following keywords.

${page}

Using ${page} enables you to designate different text for a given key based on the page.

"message.hero.title": "${message.${page}.hero.title}",
"message.index-root.hero.title": "Welcome to the home page",
"message.section-index.hero.title": "Here's a section's landing page",
"message.content-index.hero.title": "This is the content page",
"message.search-index.hero.title": "Here are your search results",
"message.error-index.hero.title": "Looks like there's an error",
${@plural} and ${@count}

Using ${@plural} and ${@count} enables you to handle plural text options for a given sum.

"plural.0": "zero",
"plural.1": "one",
"plural.#": "any",
"message.search.results-found": "${message.search.results-found.${@plural}}",
"message.search.results-found.zero": "No results found",
"message.search.results-found.one": "One result found",
"message.search.results-found.any": "${@count} results found",

Heretto Deploy Portal SSO - Introduction

This document describes high level SSO functionality of the Heretto Deploy Portal.

What is SSO?

Maintaining user identities, roles, and groups is a difficult job, and is often one that Service Providers (SPs) would often rather not do as it does not reflect their core competency. A popular solution is to delegate the responsibilities of managing user accounts and authentication to a third party entity Identity Provider (IDP). SSO, an acronym for Single Sign-On, is a family of authentication schemes which use IDPs to manage user accounts and authenticate users, and to transfer only relevant attributes back to the Service Providers. The name comes from the idea that you only have to sign-on in a single place, your IDP, and then various SPs simply contact the IDP to determine your identity in lieu of managing your personal account details and authentication themselves.

Tip:

True single sign-on enables the user to log in once and access services without re-entering authentication factors.

Important:

This document makes multiple references to "resources". A resource is simply any content or service that a Service Provider may offer to viewers. It could be an image, an article, a paragraph, or even access to some feature, such as a mortgage calculator.

To take full advantage of SSO, The SP must be able to distinguish which of it's resources are protected. In those instances, if a user is either not currently identified by the SP, or if the user has insufficient privileges to access the protected resource, then they are typically presented with a list of one or more partner IDPs by which they will be able to authenticate with, and hopefully thus gain access to the protected resource.

Tip:

A User Agent is typically a web browser, but in some specialized cases, it may not be. It may in fact be a server or some other program than a web browser.

When user agents are redirected to the IDP, the link includes the information about which page they had originally requested (this is the state). From there, the IDP confirms the user's identity, and then redirects the user agent back to the SP with an assertion and the original state of the request. This response is then processed by the SP, after which, the SP will decide whether the user is authorized to access the requested resource at that point.

Attention:

Authentication and authorization are often confused, and it is helpful to establish a good working definition of what they mean. Authentication is the process of proving that you are who you say you are. It's sometimes shortened to AuthN. Authorization is the act of granting an authenticated party permission to do something. It specifies what data you're allowed to access and what you can do with that data.

The following sequence diagram illustrates how SSO works, generally speaking, in relation to the Portal:

Figure 1. Portal Single Sign On
Sequence Diagram for user agents, Portal Sites, and IDPs
Tip: JSON Web Token (JWT) is an open standard (

RFC 7519

) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. Assertions are simply trusted details about a user that an IDP shares with an SP. It is from these details that authorization decisions can be made.

Audiences and Content Filtering

Defines the core concepts of Audiences and Content Filtering.

Roles and Audiences Overview

Once we use an external IDP to authenticate a user, and we receive an assertion back from that service, we're now ready to put that user data to work in helping us filter content properly for different audiences.

The way in which we'll do this is by identifying users as part of one or more audiences. These audiences can then be used to filter content within Heretto CCMS and ultimately, within the Heretto Deploy Portal. We'll follow up by creating DITAVAL files which will then either include or exclude content on the Heretto Deploy Portal for various site sections, topics, or even elements of the website.

What is an Audience?

An audience is any group of viewers that you want to filter content for. By filtering content, this may sometimes mean that you only want to show content for one or more audiences, or that you want to ensure that a specific audience cannot observe the content.

Oftentimes, audiences are defined as either one or more roles. For instance, your organization may have various roles, such as Doctor, Dentist, and so forth. You can make each one of those roles a separate audience, or you may combine them into a singleseparate audience where they are grouped together. For instance, you might group them both into an audience of "Health Providers".

What is Content Filtering?

CSimply put, content filtering means that you show different content for different audiences. IAnother way of phrasing this is to say that if a user can view some content, they are authorized to view that content. In order to filter content, we must do a few things to prepare for this:

  1. Have a mechanism for authenticating users, unless all of the content is going to be public

    1. We will use OpenID Connect to authenticate users with our IDP in this example

  2. Categorize our content to specify what audiences it is or is not intended to be viewed by

  3. Determine how we will translate user role information that we get from the IDP into audience attributes.

    Tip:

    If you have direct control over the IDP, you can possibly skip this step if you ensure the payload for assertions follows a naming convention which will be described later in this document

  4. Configure your sitemap so that it has rules that are clearly specified

To get started, it is important to understand that DITA has some powerful tools that enable you to define items like the targeted platforms or audiences that you intend the content to be filtered by. As well, as it relates to Heretto Deploy Portal, you can specify the audience attribute in as low or as high level specificity as you want. Consider the following snippets of the sitemap and the ditaval file that the sitemap refers to. We're going to use the "audience" attribute at the sitesection level to define content for which we are going to filter on. We also have to specify <data> elements in the ways that are detailed below so that proper ditavals are specified.

Warning:

You should familiarize yourself with DITA's conditional processing features before you attempt to filter your site'ss' content. http://docs.oasis-open.org/dita/dita/v1.3/errata02/os/complete/part3-all-inclusive/archSpec/base/condproc.html#condproc

 //sample_sitemap.ditamap
<sitesection audience="health_providers">
   <topicmeta>
       <navtitle>Authenticated Section</navtitle>
    </topicmeta>
    <mapref format="ditamap"                        href="../../Sample_Content/Policy_Manual/data_security_and_retention_policy.ditamap"/>
</sitesection>
...
<data href="filter/private.ditaval" name="content-api-audience" value="health_providers"/>
 //filter/private.ditaval
<val>
    <prop action="include" att="audience" val="health_providers"/>
</val>
Important:

The convention for specifying audience ditavals is to create <data> elements in your sitemap which have the reserved name of "content-api-audience". These data elements will only be picked up and interpreted properly if they comply with this convention. This data element then references the ditaval file via the "href" attribute. While we use ditaval files in a conventional way, the DITA specification is vague about how to configure your processing engine to process those ditaval files. We have chosen to do so using the <data> element within sitemaps.

Important:

There is an additional <data> element that you can define, with the attribute of "default-content-api-audience". There can only be one of these. It is very typical to set this to an empty value, meaning that this is what visitors who are NOT authenticated will see when they visit the site. In the example below, this indicates that by default, all content will be included for general audiences, but that content for "health_providers" will be hidden.

<data href="filter/public_only_filter.ditaval" name="content-api-default-audience" value=""/>
<!-- filter/public_only_filter.ditaval -->
<val>
    <prop action="include" att="audience" backcolor="" color="" style="" val=""/>
    <prop action="exclude" att="audience" backcolor="" color="" style="" val="health_providers"/>
</val>

When we inspect the private.ditaval file, it becomes clear that if content (which is referred to within this sitemap) has an "audience" attribute that is set to "health_providers", that it will only be included for members of the "health_providers" audience.

Notice in the example sitesection below how the audience attribute is set to "health_providers". When we evaluate all of the pieces, we see that we have:

  1. One or more <data> elements are included that follow our conventions

  2. One or more ditaval files that correspond the <data> elements

  3. One or more audience attributes specified in the content

<sitesection audience="health_providers">
    <topicmeta>
        <navtitle>Authenticated Section</navtitle>
    </topicmeta>
    <mapref format="ditamap" href="../../Sample_Content/Policy_Manual/data_security_and_retention_policy.ditamap"/>
</sitesection>

Search Indexing

Content Portal includes a very powerful search engine that actually indexes content separately for each audience grouping. For instance, it will index the public content and serve up those search results for unauthenticated users, and separately index the content that is available for "healthcare_providers", given the example that we've been using.

This is very powerful as once a user is logged into the system, they are only given search results which they can access. This avoids the awkward situation where users are not able to view content that they found in search results. This happens automatically and there is nothing further that you have to do to configure it to work, than to define the audiences properly per this document. If all of a site's content is public (i.e. there are no filtering rules), then all content in the sitemap will be available for all users.

Understanding JWT and users, roles, and audiences

JWT (pronounced "jot") is a standard for signing, encrypting, and encoding standard JSON objects. These objects are secure and are base 64 encoded. For instance, the encoded form of this JSON object:

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022,
  "content_audiences": ["healthcare_providers"]
}

Will get encoded to look something like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJjb250ZW50X2F1ZGllbmNlcyI6WyJoZWFsdGhjYXJlX3Byb3ZpZGVycyJdfQ.zNv5uUJcL_lwcl0Z5FOAewIYaW8K95flAG70mxPZ7uM

There are some standard types of data that are typically encoded into the payload of JWTs, some of these include:

  1. User Data (i.e. email address, username, job description)

  2. Roles (i.e. administrator, DBA, doctor)

  3. Audiences (i.e. "healthcare_providers")

There are some standard fields that Deploy Portal will pick up and use, if they are defined. For instance, "content_audiences", which should be a JSON list. If you have direct control over the payload fields that your IDP will deliver in its assertions, then the best bet is to go with the default field names, as no additional configuration will have to be defined, however, if you are unable or unwilling to change these names to Heretto's conventions, then there is a way (which will be detailed below) in which you can define the names of the fields to be used for each of the user, role, and audience purposes.

The full documentation for all available configurable fields for configuring an auth strategy are here: IAuthStrategy.json but we'll analyze a few of these fields for clarity:

First, let's examine the audience claim. It's documentation is as follows:

/**
     * @description JWT key name for accessing the proper field to use the content/audiences for the logged in user.
     * @default "content_audiences"
     * @example
     * If the incoming JWT is formed as:
     * {"exp": 2147483647, "content_audiences": ["private"]}
     * No additional configuration is required.
     *
     * If the incoming JWT is formed as:
     * {"exp": 2147483647, "https://jorsek.com/content/audiences": ["private"]}
     *
     * the authStrategy object would need the following parameter defined:
     * "authStrategy": {
     *   "audienceClaim": "https://jorsek.com/content/audiences"
     * }
     *
     */
    audienceClaim?: string;
    

What this indicates is that the default field name for audienceClaims is "content_audiences", and the type of that field is a JSON list (must include square brackets).

However, if you store audiences under a field named "audienceNames", which will be part of the JSON payload, then you just have to specify audienceClaim: "audienceNames" in your JSON configuration for this strategy. Then, Deploy Portal will convert all claims with name "audienceNames" into an internal claim name of "content_audiences". For instance, if part of the JWT claim for a user is:

{audienceNames: ["healthcare_providers"]} 

Then they will see the content that is provisioned only for "healthcare_providers", as this document has described.

The same set of rules hold true for these other fields, although they are not used directly for audience filtering:

/**
     * @description JWT key name for accessing the proper field to use the portal_role for the logged in user.
     * @default "portal_role"
     * @example
     * If the incoming JWT is formed as:
     * {"exp": 2147483647, "portal_role": "contributor"}
     * No additional configuration is required.
     *
     * If the incoming JWT is formed as:
     * {"exp": 2147483647, "https://jorsek.com/portal_role": "contributor"}
     *
     * the authStrategy object would need the following parameter defined:
     * "authStrategy": {
     *   "roleClaim": "https://jorsek.com/portal_role"
     * }
     */
    roleClaim?: string;
    /**
     * @description JWT key name for accessing the proper field to use the ezd_username for the logged in user.
     * @default email||username||sub||hd;
     * @example
     * If the incoming JWT is formed as:
     * {"exp": 2147483647, "email": "contributor"}
     * No additional configuration is required.
     *
     * If the incoming JWT is formed as:
     * {"exp": 2147483647, "https://jorsek.com/ezd_username": "user@contoso.com"}
     *
     * the authStrategy object would need the following parameter defined:
     * "authStrategy": {
     *   "userClaim": "https://jorsek.com/ezd_username"
     * }
     */
    userClaim?: string;
Important:

The snippets of code above are part of a larger configuration object which is authStrategy. You'll need to configure this JSON object in your config.json file in order to configure your system properly for SSO authentication. Also, if you can control the names of these fields, you can use the default field names which have been specified above without having to configure them explicitly - otherwise, you will have to configure them explicitly.

What is OAuth 2.0

This document provides a brief description of what types of problems that OAuth 2.0 solves.

Important:

Many companies use OAuth 2.0 for authentication, however, it is really intended as an authorization protocol. We recommend using OpenIDConnect in lieu of OAuth 2.0 as it is specifically designed to handle authentication, but since OpenIDConnect is based on OAuth 2.0, it is helpful to understand how it works and to use it as a knowledge base upon which we'll use to better understand OpenIDConnect.

Overview

OAuth 2.0 is the industry-standard protocol for authorization. OAuth 2.0 focuses on client developer simplicity while providing specific authorization flows for web applications, desktop applications, mobile phones, and living room devices.

What is the purpose of this?

OAuth 2.0 provides a mechanism by which different services on the Internet can inter-operate and share data and resources, without compromising a user's credentials. For instance, if you wanted one application, such as Yelp.com to be able to access your Gmail account's contact list, but didn't want Yelp to be able to log into your Gmail account and read your emails, or personal images, etc.

How does it do this?

The actual protocol is shown below. It is important to get an understanding of this because OpenID Connect is really just a specialization of OAuth.

OAuth Authorization Code Flow

The following shows the code flow of how authorizations are done in OAuth 2.0. The following diagram shows how OAuth could be used by a client in order to fetch "Profile Contacts" (address book) from accounts.google.com.

Tip:

There is no need to read this section if you're already familiar with how OAuth works.

The diagram below assumes that the yelp.com client needs to get "profile contacts" data from accounts.google.com

OAuth 2.0 Diagram

  1. First, the User Agent makes a request to the authorization server
  2. The IDP first checks to make sure that the user has been authenticated. There may be some back and forth with the user agent and the IDP to achieve authentication
  3. The IDP then checks with the resource owner (accounts.google.com) to request consent to share "profile contact" data with the client.
  4. The user agent is then redirected back to yoursite.com/callback with an authorization code.
  5. This authorization code is exchanged via a back channel for an access token. This access token is then used to get data directly from the resource owner (accounts.google.com)

At the end of this flow, the user agent has a cookie for access token, that it will pass to google.com to get access to contact information. This access token will have an expiration date, and is only good for the scope it was intended for - for instance, it cannot be used to access the user's email messages, or photos, etc.

OpenID Connect Authentication Flow

The following diagram shows the standard flow for OpenID Connect.

Note:

This diagram is essentially the same exact diagram as OAuth 2.0, with the exception of the scope being set to: "openid profile" and there is an additional step of resolving the userinfo against google's userinfo service.

OpenID Connect Diagram
  1. First, the User Agent makes a request to the authorization server
  2. The IDP first checks to make sure that the user has been authenticated. There may be some back and forth with the user agent and the IDP to achieve authentication
  3. The IDP then checks with the resource owner (accounts.google.com) to request consent to share "profile contact" data with the client.
  4. The user agent is then redirected back to yoursite.com/callback with an authorization code.
  5. This authorization code is exchanged via a back channel for an access token. This access token is then used to get data directly from the resource owner (accounts.google.com)
  6. Get the user info with the access token. This will be returned as JSON.

Understanding Delivery Portal Authn

Authentication Strategy

At its core, the Delivery Portal's SSO authentication mechanism is a configurable, and very simple state machine design. Each stage of the state machine is called a step, in Heretto's parlance. At this time, we have support for the following protocols:

  1. OAuth (not recommended)

  2. OpenID Connect (recommended)

  3. JWT

As well, we have examples of using authentication with the following OpenID Connect providers:

  1. Azure

  2. Google

  3. Keycloak

  4. Okta

The strategy configuration format is JSON, and each strategy is configured using the same general format:

  1. A list of properties

  2. Followed by a list of steps

Starting with the first step, each step, once configured, leads to the processing of the next step, and so forth. The end result of the processing is that the user has been authenticated, and that we have the following information about that user:

  1. User Claims

  2. Role Claims

  3. Audience Claims

Let's consider how we'd configure Google for OpenID connect:

"google": {
        "idp": "google",
        "client_id": "client_id",
        "client_secret": "client_secret",
        "aud": "aud",
        "iss": "accounts.google.com",
        "grant_type": "authorization_code",
        "response_type": "code",
        "scope": "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
        "hd": ["jorsek.com"],
        "redirect_path": "/auth/google",
        "auth_success_redirect": "/?state=${state}",
        "auth_uri": "https://accounts.google.com/o/oauth2/auth?redirect_uri=${base_uri}${redirect_path}&client_id=${client_id}&response_type=${response_type}&scope=${scope}&state=${state}&access_type=offline&include_granted_scopes=true&prompt=consent",
        "steps": [
            {
                "name": "token",
                "uri": "https://oauth2.googleapis.com/token?code=${auth.code}&client_id=${client_id}&client_secret=${client_secret}&redirect_uri=${base_uri}${redirect_path}&grant_type=${grant_type}&state=${state}",
                "method": "POST",
                "config": {
                    "headers": {
                        "Content-Type": "application/x-www-form-urlencoded"
                    }
                }
            },
            {
                "name": "jwt",
                "uri": "https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=${token.id_token}"
            },
            {
                "name": "refresh",
                "uri": "https://oauth2.googleapis.com/token?refresh_token=${refresh_token}&grant_type=refresh_token&client_id=${client_id}&client_secret=${client_secret}",
                "method": "POST",
                "config": {
                    "headers": {
                        "Content-Type": "application/x-www-form-urlencoded"
                    }
                }
            }
        ]
    },
Important:

The Google example above is one of several base strategy configurations that you can use. You can override any of the properties you want with your own configuration details and inherit the rest of the properties. This means that with minimal configuration, you can use Google or any of the listed providers that we already support.

As the Google example above is a full example of configuration, it is pretty complex, so we're going to break this down in chunks.

idp

This field indicates which strategy you want to use. These are the current options.

idp: "okta" | "azure" | "google" | "keycloak" | "custom" | "jwt";
Tip: If you want to implement a strategy which is not listed, you would use "custom", and then define the steps you want to follow for that implementation.
client_id
This is the client id that you are given from your partner IDP. This is how they identify your SP and validate that you are passing in the correct secret
client_secret
This is the secret that your partner IDP has given you to sign and encrypt requests with
aud
aud is short for audience and is a standard field for JWTs. If you set this field, then it will check to make sure that one of the values specified in the return JWT under the field name "aud" will match the value you specify here.
Remember: This is an optional field, but if you specify it, a match must happen, or users won't be validated on their return from the IDP to your Heretto Deploy Portal
iss
iss is short for issuer and is a standard field for JWTs. If you set this field, then it will check to make sure that one of the values specified in the return JWT under the field name "iss" will match the value you specify here.
Remember: This is an optional field, but if you specify it, a match must happen, or users won't be validated on their return from the IDP to your Heretto Deploy Portal
grant_type
grant_type is a field that is used for token requests, and is specific to OIDC. At this point in time, we only Heretto Deploy Portal "authorization_code" as the value for this field. Note: your client will typically have to be configured to support one or more grant types at the IDP, so you will have to make sure that it is configured to accept "authorization_code" grant_types for it to successfully work with our Heretto Deploy Portal.
response_type
response_type is an OpenID Connect field. There are multiple types of flows that OpenID Connect supports. The most common ones, which support web browsers as User Agents, are typically Authorization Code Flow, Implicit Flow, and Hybrid Flow. At this time, Heretto Deploy Portal only supports Authorization Code Flow as it is considered to be more secure and doesn't expose the end token to the front-channel. Therefore, if you are using OpenID Connect, you must set this field, and it should be set to "code"
scope

This field is required for OpenID Connect. scope value is not present, the behavior is entirely unspecified. Other scope values MAY be present. Scope values used that are not understood by an implementation SHOULD be ignored. This field tells the IDP what types of information you are requesting to learn about the user you are authenticating. This will then be used by the IDP to do tasks, such as inform the end user that their email address and personal information are to be shared, and to seek permission to do so.

For Google specifically, you can explore what options you have to include in the scope element. https://developers.google.com/identity/protocols/oauth2/scopes

hd

hd is a list of domains, such as ["myPrimaryDomain.com", "mySecondaryDomain.com"]. This field is used as a way to limit the domains of the users who are permitted access to your site. If this field is set, then only users who are part of this domain, as determined by the following logic, will be permitted access:

const domainToTest = jwtObj.hd || jwtObj.email || jwtObj.username || jwtObj.sub

From looking at this code, you can see that the return jwt object is evaluated to see first if it has an hd field, if so, that is used, otherwise the email, the username, and the sub, in that order. if ["hotmail.com"] was set as hd, and the returned jwt didn't have an hd field, but the user's email was "john@hotmail.com", then it would be validated, however, if the user's email was "jane@yahoo.com", then it would not be validated.

redirect_path

This is the path that the IDP will redirect users to after they have been authenticated by the IDP. For this to work correctly, there are always two parts of this, the first part is "/auth/" the second part is the name of the IDP in the idp field. This has to be configured correctly for the Heretto Deploy Portal to properly process the IDP response.

auth_success_redirect

This is the path that the user agent will be redirected to after a successful authentication.

Remember:

This configuration reflects a special path, /?state=${state} This path will automatically redirect to whatever path is indicated in the state variable.

auth_uri

This is the first path that a user will be redirected to to initiate the authentication process at the IDP level.

steps

Each step is a step in the workflow where we build upon the previous steps in order to authenticate a user. For instance, the auth_uri is used to initiate a google OIDC sequence, but we must process the request that comes back to make an additional call to get the token back.

Any data returned by a previous step can be used in further template strings* with this name. for example, step2 may need information from step1 in the request uri and can be set as follows:* "https://some-idp.com/oauth2/v3/tokeninfo?id_token=${step1.id_token}".*
Tip: A template string is simply a string that is processed to create a string from an expression. if the previous step was named step1, and a value came back with a key of foo=bar, then we could access that value in step 2 with ${step1.foo} which would evaluate to bar after it is processed.
Warning:

If the step name is "jwt", or matches the "jwt_key" defined in the IAuthStrategy object, we will automatically* validate it.

The typescript interface for each step is as follows:

/**
     * @description the name of the flow step. any data returned by this step can be used in further template strings
     * with this name. for example, step2 may need information from step1 in the request uri and can be set as follows:
     * "https://some-idp.com/oauth2/v3/tokeninfo?id_token=${step1.id_token}".
     * If the step name is "jwt", or matches the "jwt_key" defined in the IAuthStrategy object, we will automatically
     * validate it.
     * @default step${index}
     * @example "step1", "step2"
     */
    name: string,
    /**
     * @description If provided, the step will attempt to JWT decode the value defined.
     * Typically this would be used in lieu of an /introspect endpoint if the previous
     * step returns a value as one of its properties.
     * @default null
     * @example <caption>our default keylcloak config</caption>
     *
     * ```json
     [
     {
            "name": "token",
            "uri": "${auth_endpoint}/token",
            "method": "POST",
            "params": {
                "redirect_uri": "${base_uri}${redirect_path}",
                "client_id": "${client_id}",
                "code": "${auth.code}",
                "grant_type": "authorization_code"
            },
            "config": {
                "headers": {
                    "Content-Type": "application/x-www-form-urlencoded"
                }
            }
     },
     {
        "name": "jwt",
        "decode": "${token.id_token}"
     }
     ]
     ```
     *
     */
    decode?: "${token.id_token}"
    /**
     * @description the URI to make an api request
     * @example
     * "https://oauth2.googleapis.com/token?code=${auth.code}&client_id=${client_id}&client_secret=${client_secret}&redirect_uri=${base_uri}${redirect_path}&grant_type=${grant_type}&state=${state}",
     */
    uri: string;
    /**
     * @description the method to use with the request uri
     * @default GET
     */
    method?: "POST" | "GET" | "PUT",
    /**
     * @description any additional params to pass with the request.
     * NOTE: No options that are functions may be used here, only JSON.
     * @see https://github.com/axios/axios#request-config
     * @default {}
     */
    params?: IJSONObject,
    /**
     * @description Axios request config object.
     * NOTE: No options that are functions may be used here, only JSON.
     * @see https://github.com/axios/axios#request-config
     * @default {}
     * @example
     * ```json
     * { "headers": { "Content-Type": "application/x-www-form-urlencoded"} }
     * ```
     */
    config?: IJSONObject,
    /**
     * @description maps the response to different object key/value pairs so that if the IDP returns with different
     * values in the response from what you would otherwise put as keys in the resulting object, you can map them here.
     * @default null
     * @example
     * ```json
     {
    "map": {
      "id_type": "typ",
      "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": "email",
      "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/expiration": "exp"
    }
}
     *  ```
     */
    map: { [key: string]: string }

The typscript interface for an entire auth strategy is below:

/**
 * @description Authentication Strategy configuration object. Most fields are es6-syntax templatable with all values
 * defined on this object available to be used in other string fields and steps.
 */
export interface IAuthStrategy {
    /**
     * @description Identity Provider Type.
     */
    idp: "okta" | "azure" | "google" | "keycloak" | "custom" | "jwt";
    /**
     * @description Identity Provider Client ID/ApplicationID for API requests.
     */
    client_id?: string,
    /**
     * @description Identity Provider Client Secret for API requests.
     */
    client_secret?: string,
    /**
     * @description Identity Provider Audience field to verify on JWT tokens.
     */
    aud?: string,
    /**
     * @description Identity Provider Issuer field to verify on JWT tokens.
     */
    iss?: string,
    /**
     * @description Identity Provider grant_type.
     * @default "authorization_code"
     */
    grant_type?: string,
    /**
     * @description Identity Provider response_type.
     * @default "code"
     */
    response_type?: string,
    /**
     * @description Identity Provider oauth scopes.
     */
    scope?: "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
    /**
     * @description Identity Provider HD/domain field to validate against.
     */
    hd?: string[],
    /**
     * @description JWT field to use in cookies and to match on the steps to find the correct JWT step.
     * @default "jwt"
     */
    jwt_key?: string,
    /**
     * @description Redirect path for incoming oauth redirect from the IDP containing the grant_type.
     */
    redirect_path?: "/auth/google",
    /**
     * @description Redirect path for when authenitcation is fully successful.
     * @param state Used to keep track of originally requested URL.
     * @default "/?state=${state}"
     */
    auth_success_redirect?: string,
    /**
     * @description URI to redirect to when the user needs authentication. any parameters returned on the incoming redirect back are available to the auth "steps" as "auth".
     * For example, if the first step requires the "code", you can construct the first step uri with ${auth.code} as
     * "https://some-idp/token?code=${auth.code}&client_id=${client_id}&client_secret=${client_secret}&redirect_uri=${base_uri}${redirect_path}&grant_type=${grant_type}&state=${state}",
     * @example
     *
     * ```json
     * {
     *   "auth_uri":"https://accounts.google.com/o/oauth2/auth?redirect_uri=${base_uri}${redirect_path}&client_id=${client_id}&response_type=${response_type}&scope=${scope}&state=${state}"
     * }
     * ```
     */
    auth_uri?: string,
    /**
     * @description Authentication signing key.
     * This can be created in Heretto and is a pre-shared key to create individualized tokens for each user.
     */
    authSigningKey?: string;
    /**
     * @description Authenitcation steps. Run sequentially during authentication flow.
     * @see IAuthStrategyStep
     */
    steps?: IAuthStrategyStep[],
    /**
     * @description JWT key name for accessing the proper field to use the content/audiences for the logged in user.
     * @default "content_audiences"
     * @example
     * If the incoming JWT is formed as:
     * {"exp": 2147483647, "content_audiences": ["private"]}
     * No additional configuration is required.
     *
     * If the incoming JWT is formed as:
     * {"exp": 2147483647, "https://jorsek.com/content/audiences": ["private"]}
     *
     * the authStrategy object would need the following parameter defined:
     * "authStrategy": {
     *   "audienceClaim": "https://jorsek.com/content/audiences"
     * }
     *
     */
    audienceClaim?: string;
    /**
     * @description JWT key name for accessing the proper field to use the portal_role for the logged in user.
     * @default "portal_role"
     * @example
     * If the incoming JWT is formed as:
     * {"exp": 2147483647, "portal_role": "contributor"}
     * No additional configuration is required.
     *
     * If the incoming JWT is formed as:
     * {"exp": 2147483647, "https://jorsek.com/portal_role": "contributor"}
     *
     * the authStrategy object would need the following parameter defined:
     * "authStrategy": {
     *   "roleClaim": "https://jorsek.com/portal_role"
     * }
     */
    roleClaim?: string;
    /**
     * @description JWT key name for accessing the proper field to use the ezd_username for the logged in user.
     * @default email||username||sub||hd;
     * @example
     * If the incoming JWT is formed as:
     * {"exp": 2147483647, "email": "contributor"}
     * No additional configuration is required.
     *
     * If the incoming JWT is formed as:
     * {"exp": 2147483647, "https://jorsek.com/ezd_username": "user@contoso.com"}
     *
     * the authStrategy object would need the following parameter defined:
     * "authStrategy": {
     *   "userClaim": "https://jorsek.com/ezd_username"
     * }
     */
    userClaim?: string;
    /**
     * @description JWT expiration timing.
     * @default "12h"
     *
     * Either "never" or expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms).
     > Eg: `60`, `"2 days"`, `"10h"`, `"7d"`. A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default (`"120"` is equal to `"120ms"`).
     *
     * setting this value to "never" removes the exp requirement from forming a JWT. (not recommended)
     */
    expiresIn?: string;
    token?: object;
}

Important Concepts to Note

These are some important concepts that any implementations should be keenly aware of.

Overriding Parameters

It is possible to fully define a new strategy from scratch. Many times, however, you will simply want to override the settings that are provided for common OIDC providers. In that case, you use the same strategy name, and only provide keys/values for properties that you want to override. If you don't specify a property, it will just use the default configuration property that was provided in the mock_strategies file. If you do specify a key/value pair, that will override that key/value pair that was provided. This is a quick way to get started using an IDP such as google.

JWT IDP Special Handling

Please do NOT create an Authentication Strategy with the name of "jwt". This is a reserved name and trying to use this name will create problems that will be difficult to debug.

Note:

While you cannot create a Strategy named "jwt", you can and will OFTEN create a Step named "jwt" within your strategies.

JWT Decoding and Resolution

The ultimate goal of authenticating users is to en up with a JSON object that represents the user's data. In some cases, these JWT are signed and encoded, and in other cases, they are not, or may even be represented as plain JSON objects. To make implementation as simple as possible, there is special behavior regarding any step that is either named "jwt", or if the step name matches the name set on the optional configuration parameter: jwt_key.

In the Google OIDC example that was listed previously in this document, you will notice this step:

{
                "name": "jwt",
                "uri": "https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=${token.id_token}"
}

This endpoint that Google provides will return a plain JSON object of the token that was returned. As such, we don't have to take the additional step of decoding and validating a JWT. Using this service, however, is not recommended for production environments, and most of the time you will be doing the decoding yourself.

If we examine another sample of code, this time for Okta (another IDP), we can see a few steps. The first step, as was indicate earlier, is to redirect the user to the "auth_uri". This is the first implicit step. The follow on steps are:

"steps": [
            {
                "name": "token",
                "uri": "${okta_uri}/token?code=${auth.code}&client_id=${client_id}&client_secret=${client_secret}&redirect_uri=${base_uri}${redirect_path}&grant_type=${grant_type}&state=${state}",
                "method": "POST"
            },
            {
                "name": "jwt",
                "decode": "${token.id_token}"
            }
        ]

As in this case, if the decode parameter is set for a step, then the field that is referenced needs to point to a Base64 encoded JWT object. In turn, the result of that decoding will be set on the user's session, such as this:

req.session[jwt_key] = decode(jwt);

refresh_token

it is often the case that an IDP will provide you with something called a refresh token. This token can kind of be thought to be a case number for this user's authentication, and it can be used to basically refresh the user with a new token without them having to do the extra work of verifying that they want the IDP to share specific details with the SP.

If a step is named "refresh", and if upon validating a user's session it is determined that the access token has expired, then this step will be invoked with the refresh token that was assigned to the user

For instance, consider this configuration code block;

{
                "name": "refresh",
                "uri": "https://oauth2.googleapis.com/token?refresh_token=${refresh_token}&grant_type=refresh_token&client_id=${client_id}&client_secret=${client_secret}",
                "method": "POST",
                "config": {
                    "headers": {
                        "Content-Type": "application/x-www-form-urlencoded"
                    }
                }
            }

This step will NOT get called during the initial user authentication, but it will get called if there is a refresh token that came back as a field in the JWT that was returned from the IDP. For it to work properly, the IDP's initial token must have included a refresh token under the key "refresh_token".

Glossary of SSO Terms

Authentication

Confirm the identity of the user. This is where the user provides some credentials to prove they are who they claim to be.

Authorization

DANGER:

Authentication and Authorization are very different concepts. It is important to fully understand the difference between them as people often shorten either one to "auth" when discussing them

Determine whether a user has the authority to do an action or access a resource.

Identity Provider

A separate service which knows enough about the user to confirm their identity.

Service Provider (SP)

The provider of the service. In our case, the Service Provider will always be the portal itself.

Assertion

An assertion about a user that an IDP makes.

An assertion is a package of information that supplies one or more statements made by a SAML authority. SAML defines three different kinds of assertion statement that can be created by a SAML authority:

Authentication: The specified subject was authenticated by a particular means at a particular time. This kind of statement is typically generated by a SAML authority called an identity provider, which is in charge of authenticating users and keeping track of other information about them.

Attribute: The specified subject is associated with the supplied attributes.

Authorization decision: A request to enable the specified subject to access the specified resource has been granted or denied.

JWT

Attention:

pronounced as "jot"

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

In its compact form, JSON Web Tokens consist of three parts separated by dots (.), which are:

  • Header
  • Payload
  • Signature

Assertion Consumer Service

This can mean different things, but in this context, it is NOT SAML specific. In fact, we will have a generic service which will take assertions and convert them into a JWT.

Attributes

Attributes are any data that may come back from an IDP. Typically, these will be in some sort of map format. This data might be roles, groups, birthday, organizations, or any other data that is useful for granting or restricting resource authorization to users.

WAYF

This is an acronym for "Where Are You From". This page is relevant if there is more than one IDP which a user may be able to identify themselves from. For instance, if a user may be able to identify with Twitter, Facebook, and Google, this page will show them all of the available options that are configured for this site.

Client ID

This is the ID that is assigned to the Service Provider (our portal app, in this case) from the IDP. We will reference this in authentication requests that we make to the IDP so it can know who we are, and can verify our identity when it is processing such authentication requests

Client Secret

This is the secret by which we can sign authentication requests that are sent from the SP to the IDP (again, in the case of this document, from the portal to the IDP). This is very important as we want to prevent any impostors from creating a bogus request that looks like it came from our service, but is perhaps really a malicious service.

Resource Owner

Tip: This is OAuth specific

This is the owner of private information about the user who we need to authenticate and authorize. So for instance, this is perhaps the third party that will give us the names of the audiences that the user belongs to.

Client

Tip:

This is OAuth specific

The client is the system that is requesting authentication/authorization details from the IDP

Authorization Server

Tip:

This is OAuth specific

Note:

This is often, but not always separate from the Resource Server

The server which will be doing the authorization activities on behalf of the client.

Authorization Grant

The grant that proves the user has consented to share data from the third party IDP to the SP

Redirect URI

Callback URI where the User Agent will be redirected back to after authentication from the IDP

Front Channel Request

This is any request that is made directly by the browser to a server

Back Channel Request

This is a request that is made from one server to another server.

Back Channel requests are made because secure data shouldn't be stored on the browser

Scope

Indicates the scope of what the client is asking the authorization for access to. For instance, if it is asking access to "Profile contacts", but that's it, and it can't for instance get access to "email content", then this is what would be identified in the scope.

Implicit Flow

Attention:

This is not as secure as the Authorization Flow as there is no safe way to store the client secret, as such, Heretto does not support this flow.

This is a front-channel only authorization flow, that doesn't include the server making back-channel requests.

Resource Owner password Credential Flow

Attention:

Heretto does not support this flow at this time as it is not considered to be as secure as the authorization flow.

Back Channel only request flow.

Client Credentials Flow

Attention:

Heretto does not support this flow at this time as it is not considered to be as secure as the authorization flow.

Another Back Channel only request flow.

OpenID Connect

Built on top of OAuth 2.0, it is a layer which provides Identity management on top of OAuth 2.0. This is the first type of support that we will be building into our portal.