Product architecture related to authentication
Since IBM Cloud Pak for Business Automation 21.0.3 (delivered in December of 2023), the part of the product architecture relevant for authentication can be outlined by the diagram below:
There are many Cloud Pak capabilities provided by a set of microservices, which are implemented in a variety of programming languages, leveraging different frameworks and technologies: Workflow Process Service (WfPS), Navigator, and Content are just a few examples in the diagram.
In order to ensure uniform access to these capabilities from an authentication and session management perspective (and a number of additional technical aspects that benefit from uniformity), all traffic is routed through a single entrypoint, Cloud Pak foundational services' Integrated UI, which we of refer to as "Zen frontdoor" or "cpd-route", because the component originates from Cloud Pak for Data and continues to use a cpd-... hostname even in IBM Cloud Pak for Business Automation.
The Zen frontdoor component integrates with Cloud Pak foundational services' Identity Management (IM) for authentication.

When you access any of the user interfaces of the capabilities in IBM Cloud Pak for Business Automation, your browser is sending a cookie-free initial request to Zen. It will be redirected to IM, which can display its own login page to allow users select an authentication scheme. By default, you likely see the following three options.

More options can be added by integrating with an external identity provider using SAML or OpenID Connect (OIDC). A few sample configurations are documented in the CPfs IM docs. The selection of authentication schemes can be skipped by setting a default scheme.
This all works well for browser users, but for programmatic access, you need a different solution that is not relying on a redirect to a login page or some external provider.
Authentication for programmatic access
At the Zen frontdoor level, by default, the only accepted credentials are
- A Zen issued token in a cookie for an existing UI session
- A Zen API Key, see Authorizing HTTP requests by using the Zen API key
- A Zen issued token in a HTTP request header Authorization: Bearer <zen-token>
For (3), there is an API available to exchange an CPfs IM Token for a Zen token, as I described in API access tokens in Cloud Pak for Automation 21.0.3. While it is possible to obtain the IM token using standard OAuth 2.0 and OIDC flows, the additional step of exchanging the IM token for a Zen token is non-Standard and can therefore be a hassle for client applications.
Zen extension point: custom authentication service
There is a feature in Zen that allows authentication with custom tokens. It is very easily configured and as flexible as a feature can be. If you have some background in IBM WebSphere (traditional or Liberty), you can think of it as a simplified "Trust Association Interceptor (TAI)", however, the interface is not Java, but HTTPS.
The feature is documented for Cloud Pak for Data 4.8: Enabling Cloud Pak for Data to authenticate users against a custom authentication service
In essence, the custom-authentication-service extension point in Zen
- Needs to identify the situation that a custom-authentication-service needs to be invoked.
This is achieved by requiring the custom token to be sent in a custom HTTP request header. The name is configurable, but you must not use Authorization.
- Invokes a configured custom-authentication-service endpoint.
You need to provide the endpoint's URL and signer certificate to allow HTTPS communication. In addition you can specify how the custom token is transmitted to that service by providing a header name and optionally a type. The type is useful, if you want to prefix the token, e.g. when sending it as an OAuth 2.0 or OIDC token to some validation endpoint, you may say that the header name is Authorization and the type is Bearer.
- Interpreting the custom-authentication-service's response.
This is either 200 OK with a response payload in JSON format that contains the username in a configurable field or anything else in case the token was invalid, expired or not acceptable for any other reason. By default, the username is expected in a field "username", however, if your custom-token-service endpoint happens to be for example a OIDC /userinfo endpoint, then the username would come back in a field "sub" and you are free to configure that.
Configuring Zen to use a custom authentication service
The configuration is therefore as easy as follows:
keytool -printcert -sslserver oidc.sample.ibm.com:8443 -rfc > /tmp/ocp/ums.crt
export PROJECT_CPD_INST_OPERANDS=cp4ba
export CUSTOM_AUTH_HANDLER=https://oidc.sample.ibm.com:8443/custom-auth/tokenConsumer.jsp
export CUSTOM_AUTH_HEADER=X-Custom-Token
export CUSTOM_TOKEN_HEADER=Authorization
export CUSTOM_AUTH_TYPE=Bearer
export CUSTOM_AUTH_CERT=/tmp/ocp/ums.crt
export CUSTOM_USERNAME_KEY=username
oc create secret generic custom-auth-handler-secret -n ${PROJECT_CPD_INST_OPERANDS} \
--from-literal=handler=${CUSTOM_AUTH_HANDLER} \
--from-literal=header=${CUSTOM_AUTH_HEADER} \
--from-literal=customTokenHeader=${CUSTOM_TOKEN_HEADER} \
--from-literal=customTokenAuthType=${CUSTOM_AUTH_TYPE} \
--from-file=certificate=${CUSTOM_AUTH_CERT} \
--from-literal=customUsernameKey=${CUSTOM_USERNAME_KEY}
oc delete pod -n=${PROJECT_CPD_INST_OPERANDS} -l component=ibm-nginx
watch oc get pod -n=${PROJECT_CPD_INST_OPERANDS} -l component=ibm-nginx
Let's go through this one step at a time.
(1) For my sample environment, I installed a Liberty server with openidConnectServer-1.0 feature, connected to the same LDAP server as my CP4BA instance on a machine with hostname oidc.sample.ibm.com listening to port 8443 for https. I used Java's keytool to download its certificated in a file /tmp/ocp/ums.crt in PEM format, that is
keytool -printcert -sslserver oidc.sample.ibm.com:8443 -rfc > /tmp/ocp/ums.crt
cat /tmp/ocp/ums.crt
-----BEGIN CERTIFICATE-----
MI...oll
-----END CERTIFICATE-----
(2) My CP4BA instance is installed in a namespace "cp4ba" in my OCP cluster. I am using version 24.0.1 (no iFixes yet), which comes with Zen 6.0.4.
export PROJECT_CPD_INST_OPERANDS=cp4ba
(3) As specified in the OpenID Connect spec, my liberty server is providing a /userinfo endpoint at https://oidc.sample.ibm.com:8443/oidc/endpoint/ums/userinfo , however, to show the inner workings of the feature, I deployed a trivial sample application that dumps all incoming HTTP headers and statically responds with a username, no matter what was passed in.
export CUSTOM_AUTH_HANDLER=https://oidc.sample.ibm.com:8443/custom-auth/tokenConsumer.jsp
The remaining settings relate to
- How Zen extracts the custom token from incoming request: From a HTTP request header X-Custom-Token
export CUSTOM_AUTH_HEADER=X-Custom-Token
- How Zen sends the token to the custom-auth-service endpoint: In a Header Authorization, prefixed with Bearer.
export CUSTOM_TOKEN_HEADER=Authorization
export CUSTOM_AUTH_TYPE=Bearer
- How Zen extracts the username from the custom-auth-service's response: by getting the "username" field.
The docs first exported all these environment variables and then use these variables when creating a kubernetes secret. I followed the same pattern.
oc create secret generic custom-auth-handler-secret -n ${PROJECT_CPD_INST_OPERANDS} \
--from-literal=handler=${CUSTOM_AUTH_HANDLER} \
--from-literal=header=${CUSTOM_AUTH_HEADER} \
--from-literal=customTokenHeader=${CUSTOM_TOKEN_HEADER} \
--from-literal=customTokenAuthType=${CUSTOM_AUTH_TYPE} \
--from-file=certificate=${CUSTOM_AUTH_CERT} \
--from-literal=customUsernameKey=${CUSTOM_USERNAME_KEY}
Finally, the Zen pods need to be restarted to pick up the configuration change.
oc delete pod -n=${PROJECT_CPD_INST_OPERANDS} -l component=ibm-nginx
watch oc get pod -n=${PROJECT_CPD_INST_OPERANDS} -l component=ibm-nginx
Upon restart, you should see that the secret was found and processed:
oc logs -f -l component=ibm-nginx
...
custom-auth-handler-secret cert exists.
...
Sample service and invocation
Now, let's see the effect of that in action.

I defined a variable with my OCP hostname and then invoked a the "current user details" REST API in Workflow Process Service in IBM Cloud Pak for Business Automation 24.0.1 using a "random" token value of test123. The response is user details for an arbitrary username, which is 117713724.
So, obviously, no Zen token was involved and we were still able to call the REST API. That is, the custom-auth-service extension point was happy with my "random" token test123 passed in a X-Custom-Token header. Why is that?
As configured, Zen extracted the value "test123" from the incoming request header X-Custom-Token and sent it to my configured endpoint at https://oidc.sample.ibm.com:8443/custom-auth/tokenConsumer.jsp. Here is its implementation:

It simply dumps all request headers in to log and statically returns that the username is 117713724. All other JSON fields will be ignored. Let's inspect the log:

Indeed, my application was called by a client that identifies as "lua-resty-http" (which happens to match the Zen implementation). Among the incoming HTTP request headers, there is Authorization: Bearer test123, which matches my configuration for CUSTOM_TOKEN_HEADER and CUSTOM_AUTH_TYPE. The JSP implementation blindly returned the hard-coded JSON body, so my username was asserted to Zen, which created its Zen token for calling to its Workflow Process Service backend.

In real life, you would not need (nor want) to dump the sensitive incoming HTTP headers in the the custom-auth-service's implementation. And of course, you would want to return a JSON response including the real username represented by the incoming token. The logic to do that is completely custom, depending on your exact token format.
Short-cut OpenID Connect access tokens
A commonly useful use case is OpenID Connect (OIDC). You may have already set up CPfs to redirect browser users to an OIDC Offering Party (OP) as identity providers are referred to in the OIDC spec. The custom-auth-service feature can allow you to also accept OIDC tokens (OAuth 2.0 access_tokens), without having to implement the custom-auth-service by yourself!
As my custom-auth-service implementation happens to run on a WebSphere Liberty server, with its openidConnectServer-1.0 feature enabled. With that, I can register a new OIDC client application:
oidc=https://oidc.sample.ibm.com:8443
oidcadmin=<some-admin>
oidcpwd=********
curl -ski -X POST -H "Content-Type:application/json" -u "$oidcadmin:$oidcpwd" -d @- "$oidc/oidc/endpoint/ums/registration" <<EOF
{
"scope": "openid",
"preauthorized_scope": "openid profile",
"introspect_tokens": true,
"client_id": "customApp",
"client_secret": "passw0rd",
"client_name": "customApp",
"grant_types": ["implicit", "authorization_code"],
"redirect_uris": ["https://localhost"],
"response_types": ["token", "code", "id_token", "id_token token"]
}
EOF
echo "$oidc/oidc/endpoint/ums/authorize?client_id=customApp&redirect_uri=https://localhost&state=123&scope=openid%20profile&response_type=id_token%20token&nonce=abc"
At runtime, this client app would obtain a token from the OIDC OP by redirecting the user to the URL returned by the final echo statement in the code sample above.
https://oidc.sample.ibm.com:8443/oidc/endpoint/ums/authorize?client_id=customApp&redirect_uri=https://localhost&state=123&scope=openid%20profile&response_type=token
After successful authentication, my browser is redirected to https://localhost/#session_state=LU03JqyIqTSfvNr7VNq8cAqyoomWvf%2FuIkeLS8tYyAo%3D.11fe9f3e27323&scope=openid&access_token=p9OsqyCYR4JlVq2fvSF33Uk0fnndMqyxkFJw6XG3&token_type=Bearer&expires_in=7199&state=123 and since there is no application registered for listening on localhost:80, I can just copy the value of the access_token from the URL bar.
Per OIDC spec, my OIDC OP provides a /userinfo endpoint. Sending a GET request with a current access_token.

So this /userinfo endpoint can return the username represented by the access_token in a field called "sub". This observation allows us to tweak the configuration:
export PROJECT_CPD_INST_OPERANDS=cp4ba
export CUSTOM_AUTH_HANDLER=$oidc/oidc/endpoint/ums/userinfo
export CUSTOM_TOKEN_HEADER=Authorization
export CUSTOM_AUTH_HEADER=X-Custom-Token
export CUSTOM_AUTH_TYPE=Bearer
export CUSTOM_AUTH_CERT=/tmp/ocp/ums.crt
export CUSTOM_USERNAME_KEY=sub
oc delete secret custom-auth-handler-secret -n ${PROJECT_CPD_INST_OPERANDS}
oc create secret generic custom-auth-handler-secret -n ${PROJECT_CPD_INST_OPERANDS} \
--from-literal=handler=${CUSTOM_AUTH_HANDLER} \
--from-literal=header=${CUSTOM_AUTH_HEADER} \
--from-literal=customTokenHeader=${CUSTOM_TOKEN_HEADER} \
--from-literal=customTokenAuthType=${CUSTOM_AUTH_TYPE} \
--from-file=certificate=${CUSTOM_AUTH_CERT} \
--from-literal=customUsernameKey=${CUSTOM_USERNAME_KEY}
oc delete pod -n=${PROJECT_CPD_INST_OPERANDS} -l component=ibm-nginx
watch oc get pod -n=${PROJECT_CPD_INST_OPERANDS} -l component=ibm-nginx
The two important updates are
- CUSTOM_AUTH_HANDLER in order to call to the /userinfo endpoint
- CUSTOM_USERNAME_KEY in order to read the response JSON field "sub"
Indeed after restarting the Zen service, the request with token123 fails, as this is not an acceptable token for the OIDC provider's /userinfo endpoint. Unfortunately, it fails with HTTP 500, which may be considered a bug.

Calling the current user's details REST API in Workflow Process Service with the access_token retrieved from the same OIDC provider works.

Looking at HTTP access logs of the OIDC provider, we can see token retrieval, a successful request to /userinfo using the valid access_token, and 401 failures when I took the screenshots with test123.

Conclusion
The custom auth service extension point in Zen is a very powerful and simple way to allow IBM Cloud Pak for Business Automation 24.0.0 and later to accept custom authentication tokens for programmatic API access. With the very simple interface, you may already have an implementation at hand, such as the /userinfo endpoint in OIDC, which can be used for access_tokens.