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:

  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. 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 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;
}