BPM, Workflow, and Case

 View Only

Retrieve and Propagate JWT for Authenticating Downstream Services

By SHUO ZHANG posted Mon November 15, 2021 10:30 AM

  

IBM Business Automation Workflow(BAW) delegate it’s authentication to WebSphere Application Server(WAS), as WAS can be configured as OpenID connect relying party, BAW support this mechanism also to achieve single sign-on (SSO) with IBM or third-party solutions which share same OpenID connect provider(IdP).

When BAW is configured as relying party, after user login portal, workflow server side will hold an ID Token (JWT), sometimes the business scenario requests workflow engine propagate this JWT to the following downstream services. Typically, these downstream services will be configured to use basic auth for the authentication which is configured at server level. When use JWT, the downstream services will be invoked by “current” user’s permission but not the pre-configured functional ID.

This article will cover how to retrieve and propagate JWT and use it to authenticate downstream services, the JWT can be the token of user who login Workflow portal. Nevertheless, the similar methodology can be used at the scenario which the JWT passed from invoking REST API of Workflow. I will use another article to cover how to use Trust Association Interceptor (TAI) authenticate user use external JWT to invoke BAW REST API.

First, let’s go through the challenges for the whole JWT propagation path:

  1. After configured the BAW as OpenID connect relying party, where we can retrieve JWT?
    When we configure BAW(or WAS) as OpenID connect relying party, we need enable TAI, by the implementation of ibm.ws.security.oidc.client.RelyingParty. This java class will help you redirect the authentication request to the IdP and process the authentication flow. This class will get JWT from the provider and store it as ID token(id_token) in JAAS security context.
  2. How to retrieve JWT from JAAS security context and propagate it?
    As BAW do not guarantee the JAAS security context is persistent at the whole process life cycle, we need get JWT from JAAS security context as soon as possible – for Client Side Human Service (CSHS) we need create a Java external service to retrieve JWT from JAAS security context at this CSHS scope, then store it as process variable. According for your business logic, you can pass it to the following activities and destroy it after usage.
  3. How to use JWT invoke downstream REST services?
    BAW interact downstream REST services by OpenAPI specification. At OpenAPI, it supports header parameters. The header parameter will be sent with an HTTP request as custom header. So, we need define a header parameter at OpenAPI definition file for the API which accept JWT as authentication method. Since we already have a process variable hold JWT, the JWT can be passed when invoke the REST service.

This chart present high-level procedure for proving the concept

This chart present high-level procedure for proving the concept

Detail Steps for proving JWT retrieve and propagation

Enable BAW as OpenID connect relying party
Since this topic can be a separate article, I only cover it at very high-level, basically I followed two documents to configurate it:

  1. Configuring an OpenID Connect Relying Party
  2. Configuring third-party authentication products

Here is my customized procedure:

  1. Register a new client at IdP for BAW
    The API for client registration is vary for different IdPs, you need follow your IdP document to invoke the registration API. Here is my sample registration json:
    {
        "token_endpoint_auth_method": "client_secret_basic",
        "client_id": "baw",
        "client_secret": "please-change-it",
        "scope": "openid profile email",
        "grant_types": [
            "authorization_code",
            "client_credentials",
            "implicit",
            "refresh_token",
            "urn:ietf:params:oauth:grant-type:jwt-bearer"
        ],
        "response_types": [
            "code",
            "token",
            "id_token token"
        ],
        "application_type": "web",
        "subject_type": "public",
        "preauthorized_scope": "openid profile email general",
        "introspect_tokens": true,
        "allow_regexp_redirects":true,
        "redirect_uris": [
            "regexp:https://.*/oidcclient/iam.*"
        ]
    }
    

    Note: the redirect URI is defined by com.ibm.ws.security.oidc.client.RelyingParty, you should not change it, while you can modify the wild card according for your environment. The wild card in the sample json is very flexible, which can be apply for any host.

  2. add TAI - ibm.ws.security.oidc.client.RelyingParty
  3. add configuration properties for TAI

    Notes: configure TAI use realm “defaultWIMFileBasedRealm”, this is the default realm at my security domain, you can find it at “Global security” -> “User account repository” -> “Realm name”. Configure TAI use realm which is exactly same with the realm at your security domain can simplify the following steps.
  4. Import IdP’s SSL signer certificate
    Notes: need both import to the cell and node trust store
  5. Install the OpenID connect application
  6. Configure LDAP which your IdP use as the repository of Federated repositories
    Notes: Without this step, the BAW engine cannot get detail information of authenticated user from VMM.
  7. Restart servers

After configured BAW as OpenID connect relying party, you can verify it by login Process Portal. When try to login, you will be redirected to the login page of IdP. After you input correct credential, the page will be redirected back to the normal portal page and show your login user’s name at the up-right cover of the portal.

Start from here, the JWT has been retrieved from IdP and store it in JAAS security context. At the next step we will try to write a Java external service to retrieve JWT.

Implement Java external service to retrieve JWT

You can run Java external service as the permission of engine process, so the permission is enough to get the caller Subject. At this step, the Java external service will be used to retrieve JWT from JAAS security context.

To create a Java project of external service, you can select any IDE you like. Please aware that the following java code snipped need reference WAS library: com.ibm.ws.runtime.jar

package joshua.test;

import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.security.auth.Subject;

import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;

import com.ibm.websphere.security.auth.WSSubject;
import com.ibm.websphere.security.WSSecurityException;

public class SecurityContext {
    /**
     * @param tokenName input token name want to retrieve from JAAS security context
     * @return
     * @throws PrivilegedActionException
     */
    public static String getToken(String tokenName) throws PrivilegedActionException {
        System.out.println("**** Enter getToken, paramter tokenName = " + tokenName);
        String token = null;
        Subject threadSubject;
        try {
            threadSubject = AccessController.doPrivileged(new PrivilegedExceptionAction<Subject>() {
                public Subject run() throws WSSecurityException {
                    // needs javax.security.auth.AuthPermission wssecurity.getCallerSubject
                    Subject threadSubject = WSSubject.getCallerSubject();
                    if (threadSubject != null) {
                        return threadSubject;
                    }
                    // needs javax.security.auth.AuthPermission wssecurity.getRunAsSubject
                    return WSSubject.getRunAsSubject();
                }
            });
        } catch (PrivilegedActionException pae) {
            System.out.println(pae);
            throw pae;
        }
        System.out.println("**** Get Subject succeed: " + threadSubject.toString());
        if (null != threadSubject) {
            try {
                // extract the token from the public credentials
                Set<Map> publicCreds = threadSubject.getPublicCredentials(Map.class);
                if (null != publicCreds && !publicCreds.isEmpty()) {
                    for (Map<String, Object> ht : publicCreds) {
                        System.out.println("**** Get public credentials: " + ht);
                        printMap(ht);
                        token = (String) ht.get(tokenName);
                        if (token != null) {
                            System.out.println(
                                    String.format("**** Get token: %s from public credentials: %s", tokenName, token));
                            break;
                        }
                    }
                }
                if (token == null) {
                    System.out.println("**** Did not get access token from public credentials.");
                    // extract the access_token from the private credentials
                    Set<Map> privCreds = threadSubject.getPrivateCredentials(Map.class);
                    if (null != privCreds && !privCreds.isEmpty()) {
                        for (Map<String, Object> ht : privCreds) {
                            System.out.println("**** Get private credentials: " + ht);
                            printMap(ht);
                            token = (String) ht.get(tokenName);
                            if (token != null) {
                                System.out.println(String.format("**** Get token: %s from private credentials: %s",
                                        tokenName, token));
                                break;
                            }
                        }
                    }
                }
                if (token == null) {
                    System.out.println("**** Did not get access token from private credentials.");
                }
            } catch (Exception e) {
                System.out.println(e);
            }
        }
        return token;
    }

    private static void printMap(Map<String, Object> map) {
        for (Entry<String, Object> entry : map.entrySet()) {
            System.out.println("**** Get entry, key=" + entry.getKey() + "\t value=" + entry.getValue().toString());
        }
    }
}

Compile and package this java class to a jar file, then import it as a workflow server file:


Create a Java External Service base on the server file, the input parameter is the token name want to retrieve, the output value is the token in string format:

After wiring this Java external service with the Coach, you can use a Post-execution Script to check it can get the JWT back. The script can be:

log.info("**** Get JWT token: " + tw.local.jwtToken)

Note:

  1. The input token name is "id_token"
  2. The ID token is only assigned when you run CSHS through portal, if you run the service as Debug mode, there is no ID token assigned

After verified it can get JWT back as process variable, we can try to write a demo REST service to accept a JWT token and prompt it for debug purpose.

 

Define an OpenAPI definition to accept a JWT as head parameter

There are many ways to write a REST service to demonstrate this solution. Here is my sample OpenAPI definition:

openapi: 3.0.3
info:
  title: openapi-swaggerui API
  version: 1.0.0-SNAPSHOT
paths:
  /fruits:
    get:
      tags:
      - Fruit Resource
      parameters:
      - name: X-E2E-Trust-Token
        in: header
        schema:
          type: string
      responses:
        "200":
          description: OK

 

This OpenAPI definition tell client when use GET method for the /fruits api, it needs input X-E2E-Trust-Token as HTTP header. After we create a REST external service base on this OpenAPI definition, we can pass the variable(tw.local.jwtToken) as this parameter:


Something keep in mind before use it at production

  1. JWT is the confidential information for user, please do not expose it at process query or prompt it at log/trace file
  2. JWT has expiration time, try to use it as soon as possible and discard it afterward
  3. BAW switch the security context frequently at the process life cycle, try to use Java external service to retrieve it at the same security context. Eg. For CSHS, you need retrieve it at the CSHS flow scope.




0 comments
100 views

Permalink