Portal SSO Overview
This document describes high level SSO functionality of the Heretto 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.
To take full advantage of SSO, The SP must be able to distinguish which of its 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.
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.
The following sequence diagram illustrates how SSO works, generally speaking, in relation to the portal:
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 Portal. We'll follow up by creating DITAVAL files which will then either include or exclude content on the Heretto 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. Filtering content means that you want to show only some content for one or more audiences, or that you want to ensure that a specific audience cannot see some portion of the content.
Typically, audiences are defined as either one or more user-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 single audience where they are grouped together. For instance, you might group them both into an audience of Health Providers.
What is Content Filtering?
Content filtering provides a method to show different content to different audiences. Content filtering is often referred to as personalization. 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:
-
Have a mechanism for authenticating users, unless all of the content is going to be public
Note: We will use OpenID Connect to authenticate users with our IDP in this example. -
Categorize the content to specify what audiences it is or is not intended to be viewed by
-
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. -
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 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.
//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>
<data>
elements in your sitemap that 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 in the href
attribute.
<data>
element that you can define, with the attribute of content-api-default-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:
-
One or more <data> elements are included that follow our conventions
-
One or more ditaval files that correspond the <data> elements
-
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
Heretto 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:
-
User Data (i.e. email address, username, job description)
-
Roles (i.e. administrator, DBA, doctor)
-
Audiences (i.e.
healthcare_providers
)
There are some standard fields that the 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. Its 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, the 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;
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.
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.
The diagram below assumes that the yelp.com client needs to get "profile contacts" data from accounts.google.com
- First, the User Agent makes a request to the authorization server
- 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
- The IDP then checks with the resource owner (accounts.google.com) to request consent to share "profile contact" data with the client.
- The user agent is then redirected back to yoursite.com/callback with an authorization code.
- 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.
- First, the User Agent makes a request to the authorization server
- 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
- The IDP then checks with the resource owner (accounts.google.com) to request consent to share "profile contact" data with the client.
- The user agent is then redirected back to yoursite.com/callback with an authorization code.
- 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)
- Get the user info with the access token. This will be returned as JSON.
Understanding Delivery Portal Authentication
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:
-
OAuth (not recommended)
-
OpenID Connect (recommended)
-
JWT
As well, we have examples of using authentication with the following OpenID Connect providers:
-
Azure
-
Google
-
Keycloak
-
Okta
The strategy configuration format is JSON, and each strategy is configured using the same general format:
-
A list of properties
-
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:
-
User Claims
-
Role Claims
-
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"
}
}
}
]
},
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 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 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 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 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 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. Refer to: OAuth 2.0 Scopes for Google APIs
- 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 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.
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
.