This is an overview of some of the options that exist in Open Liberty to consume jwt tokens generated by WebSEAL. It is largely based on the blog post by Shane Weeden (https://community.ibm.com/community/user/security/blogs/shane-weeden1/2016/11/11/isam-902-the-jwt-sts-module-and-junction-sso-to-we), but extended to use the newer capabilities of WebSEAL.

Adding a jwt token directly to a junction without using the AAC or Federation module, is new in IBM Security Verify Access 10

This means this has become a lot simpler.

I’ve added the configuration with the microProfile JWT feature, and added some very simple samples.

  1. Components
    1. Open Liberty
    2. Containers
    3. WebSeal jwt SSO
  2. Source Material
  3. Configuration for openidconnectclient feature
  4. Configuration for mpJwt feature

Components

Open Liberty

There’s several options in Open Liberty to enable the jwt authentication. I’ve explored 2 of them:

  • openid Connect Client feature
  • MicroProfile JWT feature (mpJwt)

openid Connect Client feature

This is the OpenLiberty feature originally used in Shane’s blog.

https://openliberty.io/docs/latest/reference/config/openidConnectClient.html

It’s suitable for traditional web applications, and likely easy to integrate in existing applications.

MicroProfile JWT version 1.2

MicroProfile enables you to develop and deploy cloud-native Java applications as loosely coupled, lightweight services, each representing one unique business function.

https://openliberty.io/docs/latest/microprofile.html

This obviously also makes perfect sense in Container environments, like the OpenShift platform.

Cookies … njam njam

Now there’s a difference in the way the 2 samples operate.

The MicroProfile is meant for web services, and there’s no JSESSIONID cookie created (it’s supposed to be stateless).

However, this implementation, using the WebSeal JWT junction does use the WebSEAL session cookie to create single sign on.

The openidconnectClient sample runs a traditional web application (servlet-5.0 feature), and as such there is a JSESSIONID cookie.

Depending on the configuration of the openidconnectClient feature, you can avoid that an LTPA Cookie is sent (see disableLtpaCookie)

Containers

Some notes on Containers : I’ve prepared 2 Open Liberty Containers that run the 2 cases (mpJwt and openidconnectClient).

Podman Container

You can run the containers for Open Liberty on any other platform and connect IBM Security Verify Access to them.

I’ve used Podman myself, but Docker works too. This would only be sufficient for a test setup, not a real production setup.

Redhat OpenShift

The Openshift configuration basically runs containers.

Please note that I’ve used a setup that creates junctions to OpenLiberty applications running on Containers (in addition to Podman I’ve also used the Redhat OpenShift Platform), but my IBM Security Verify Access system does not run on Containers/OpenShift (I’m still using virtual appliances).

This means the route into the applications has to go through an exposed route/ingress, not through internal services.

I’ve used Redhat Service Mesh (https://www.redhat.com/en/technologies/cloud-computing/openshift/what-is-openshift-service-mesh) myself. There’s some additional configuration related to this Service Mesh required (although this depends on how you configure the Service Mesh). Using standard routes will work too, though. But with normal routes, you are almost obliged to use Virtual Host junctions, while with the Service Mesh, transparent path junctions can match the configuration.

I’m not actually going into the application configuration details here.

Image:OpenShift Project

TIP: What is likely necessary (depending on the configuration of the service mesh), is setting the “Server Name Indicator (SNI)” value when you create a junction.

WebSEAL

IBM Application Gateway

This is a good time to mention the IAG. This is a container with a lightweight reverse proxy.

The microProfile matches very nicely with the IBM Application Gateway.

https://docs.verify.ibm.com/verify/docs/use-ibm-application-gateway-with-ibm-security-verify-access

This lightweight reverse proxy has a similar capability to send a jwt token to backend (resource) servers.

https://docs.verify.ibm.com/gateway/docs/yaml-resource_servers-identity_headers#jwt

WebSeal jwt SSO junction

But the WebSeal solution still makes sense today. The WebSeal jwt SSO junction allows simple (session based) single sign on between your Single Page Application and the REST services running in microProfile microservices.

This is a Cookie-based session (based on the WebSeal Cookie being passed to the junctions that protect your microservices).

So on the IBM Verify Access Appliance, the big improvement is that you can generate a jwt token directly on WebSEAL, without needing the Federation nor Advanced Access Control module. It behaves more or less similar to the older LTPA single signon type : a token (cookie or header) is inserted with every request to the backend.

It’s available since version 10.0.0, but improves in later fixpacks. My setup is based on IBM Security Verify Access 10.0.4.

https://www.ibm.com/docs/en/sva/10.0.4?topic=solutions-json-web-tokens-in-http-headers

The solution using Federation and STS chains is still valid and it really depends on your use case if you can use this newer solution or not. As long as you are able to put the claims you want to put in the jwt token in the WebSeal credential (see the credential viewer app), you’re good.

This write-up just puts fixed text in some of the claims, which is fine for testing, but obviously not (always) in a real-world scenario.

If you want to use JKE (encrypted jwt), you’ll also have to use the Federation / STS chains solution. The JWT tokens generated by WebSeal can be signed, but not encrypted at present.

Source material

The source material is available through github:

https://github.com/Bozzie4/isam-liberty-jwt

So to clone this repository:

cd <directory_of_your_choice>
git clone https://github.com/Bozzie4/isam-liberty-jwt.git
cd isam-liberty-jwt

Configuration for Liberty with openidConnectClient feature

I’m using the exact same SubjectDumperEAR.ear application that I got from Shane’s blog :

https://community.ibm.com/community/user/security/blogs/shane-weeden1/2016/11/11/isam-902-the-jwt-sts-module-and-junction-sso-to-we

Container

#THIS IS THE LIGHT IMAGE BASED ON UBUNTU
# There is also an image based on ubi
FROM docker.io/library/open-liberty:kernel-slim-java17-openj9

ARG NAME=openidtest
ARG VERSION=v1.0.0

ENV TZ=Europe/Brussels \
	APP_NAME="$NAME"

LABEL \
	org.opencontainers.image.authors="Tom Bosmans" \
	org.opencontainers.image.vendor="Open Liberty" \
	org.opencontainers.image.url="" \
	org.opencontainers.image.source="" \
	org.opencontainers.image.version="$VERSION" \
	vendor="Open Liberty" \
	name="$NAME" \
	version="$VERSION" \
	summary="$NAME" \
	description="Open Liberty runtime with $NAME."

USER root

#==========================
# Add trusted certificates on Ubuntu - must be named *.crt !!!
#========================
ADD ["isam.crt", \
	"/usr/local/share/ca-certificates/"]
# Run update for ca trust - f0cabfe5.0
RUN update-ca-certificates --verbose

#USER 1001
USER default
#
# Default adding cert_defaultKeyStore
#     You can overwrite these in Podman, Docker or OpenShift
ENV cert_defaultKeyStore=/etc/ssl/certs/ca-certificates.crt

COPY --chown=1001:0 /src/main/liberty/config /config

RUN features.sh

COPY --chown=1001:0 ear/*.ear /config/dropins

RUN configure.sh

Build

The application is supplied as an .ear file.

You can build and run the container:

cd openidconnectclient/
podman build -t openidconnectclient:v1.0.0 .
podman run -d --replace --name openidtest -p 9081:9081 --add-host isam.tombosmans.eu:10.168.73.248 openidconnectclient:v1.0.0

The --add-host parameter adds an entry to the Container’s /etc/hosts file, to enable the connnection from the Open Liberty server to the jwks endpoint.

Open Liberty

Add feature

Add the openidConnectClient feature to the server.xml:

  <featureManager>
    <feature>openidConnectClient-1.0</feature>
    <feature>servlet-5.0</feature>
  </featureManager>

Enable the ports

  <httpEndpoint id="defaultHttpEndpoint"
    host="*"
    httpPort="9080"
    httpsPort="9443" />

Configure the openidConnectClient feature

Configure the openidConnectClient feature:

The important items here are

  • inboundPropagation set to required, to enable (or rather force) jwt authentication
  • issuerIdentifier: the iss claim, must match the jwt WebSeal configuration
  • jwkEndpointUrl: the jwks endpoint (points to WebSeal’s jwks app)
  • headerName: the openidConnectClient allows to use a custom headerName, in this case : jwt
  • realmName: passes the realm name to this traditional ear application.
<openidConnectClient
 id="TBJWT"
 inboundPropagation="required"
 issuerIdentifier="https://issuer"
 audiences="ALL_AUDIENCES"
 signatureAlgorithm="RS256"
 jwkEndpointUrl="https://isam.tombosmans.eu:444/jwks.json"
 realmName="defaultRealm"
 groupIdentifier="groups"
 headerName="jwt"
 mapIdentityToRegistryUser="false"
 accessTokenCacheEnabled="true"
 accessTokenCacheTimeout="10s"
 tokenReuse="true"
 disableLtpaCookie="true"
/>

Note that disableLtpaCookie is set to true here. This means that Open Liberty is NOT going to generate an LTPA Cookie.

It is therefor important to make sure the generated jwt tokens can be reused for a particular amount of time - otherwise each request will contain a new jwt token and that would have to be parsed and processed every time. That is why I’ve set the accessTokenCache* and tokenReuse properties (although 10s is way too low for real life usefulness).

Open Liberty Trust certificates

The signer CA certificate to access the jwkEndpointUrl MUST be in the default trust store, the default trust store (if not configured otherwise) is the same as the default key store : defaultKeyStore.

To add signer certificates at runtime (when Open Liberty starts) automatically, you need to create an environment variable that is named cert_<name of keystore>, so in this case : cert_defaultKeyStore.

In Linux (for instance, to run the development environment using maven), this looks like this:

export cert_defaultKeyStore=<full_path_to_ca_file>

WebSeal

Junction

This is a normal, standard junction to reach the Liberty server.

Image:API Junction

The server configuration connects to the non-ssl port of the Open Liberty container, in this case 9080.

jwt configuration

Add the following to WebSeal’s configuration file.

[jwt:/openidtest]
key-label = jwtsign
claim = text::https://issuer::iss
claim = attr::AZN_CRED_PRINCIPAL_NAME::sub
claim = attr::AZN_CRED_PRINCIPAL_NAME::upn
claim = attr::AZN_*
claim = text::[Echoer]::roles
claim = text::[groupa,groupb]::groups
include-empty-claims = false
hdr-name = jwt
hdr-format = %TOKEN%
lifetime = 0
renewal-window = 15

The lifetime set to 0 means the jwt token will remain valid for the duration of the WebSeal session. This avoids that WebSeal would need to generate a new jwt token on (almost) every request (which would possibly have a negative performance impact).

Anyway, whatever you do, don’t set the lifetime to a value lower than the renewal-window. The actual lifetime is calculated as lifetime minus renewal-window

jwks endpoint

Using the local-apps stanza, you can add the jwks application , that will create a jwks endpoint based on the SSL keystore that is assigned to the Reverse Proxy.

I also enable the cred-viewer application, because it’s a convenient way to see what’s in the credential.

[local-apps]
jwks = jwks.json
cred-viewer = credviewer

You must assign an unauthenticated ACL to this endpoint, so it’s publicly available.

Test application

So now we’ve prepared all elements (assuming you have a reverse proxy already on WebSeal and you have a test user).

dumpHeaders.jsp

I’ve added an unauthenticated ACL to the /openidtest/dumpHeaders.jsp Object in pdadmin.

Image:API Junction

Note: Accessing this endpoint with an authenticated session on WebSeal, throws this error in Open Liberty. I have not attempted to fix this.

E checkSecurity SESN0008E: A user authenticated as anonymous has attempted to access a session owned by user:defaultRealm/tommie

dump.jsp

Accessing https://your-isam-system/openidtest/dump.jsp, requires you to log on. After you log in, it dumps the content of the jwt token sent by WebSEAL, and informatio on the authenticated user.

Image:API Junction

whoami.jsp

Accessing https://your-isam-system/openidtest/whoami.jsp, equally requires you to log on. It dumps the content a summary the user data:

Image:API Junction

rolea/whoami.jsp

The resources that are only accessible for users in “role A”, are also accessible, because we put “groupa” in the groups claim in the jwt token:

Image:API Junction

Note that there is NO LtpaToken in this case (see the server.xml configuration).

Configuration for mpJwt

I’ve used this “Securing microservices with JSON Web Tokens” guide and added the creating of a Container image (for running locally with Podman or Docker, or OpenShift).

I’ve also modified the application a bit so it works with microProfile 5.0 , and I’ve added a couple of the custom Claims.

https://github.com/OpenLiberty/guide-microprofile-jwt

Podman/Docker

Container file

This is a sample container file that you can use.

Notice the isam.pem CA file; that is the public key for the self-signed certificate I’m using in my ISAM Virtual machine. Of course you would need to use your own self-signed file here.

It is important here, because OpenLiberty needs the CA trusts to connect to ssl endpoints and specifically the jwks endpoint. By adding environment variables named for the keystores in Liberty (eg cert_defaultKeyStore), these public keys can be added at runtime.

#THIS IS THE LIGHT IMAGE, BUT THEN WE DON'T HAVE FEATURES (Ubuntu based)
FROM docker.io/library/open-liberty:kernel-slim-java17-openj9

LABEL \
  org.opencontainers.image.authors="Tom Bosmans" \
  org.opencontainers.image.vendor="IBM" \
  org.opencontainers.image.url="local" \
  org.opencontainers.image.source="https://github.com/OpenLiberty/guide-getting-started" \
  org.opencontainers.image.version="$VERSION" \
  org.opencontainers.image.revision="$REVISION" \
  vendor="Open Liberty" \
  name="system" \
  version="$VERSION-$REVISION" \
  summary="The mp-jwt-sample-app" \
  description="This image contains the system microservice running with the Open Liberty runtime."

ENV TZ=Europe/Brussels
#
#  Add signer cert for isam f0cabfe5.0
#   must be named xxx.crt to work in ubuntu
#
COPY isam.crt /usr/local/share/ca-certificates/

USER root
# Run update for ca trust
RUN update-ca-certificates --verbose

USER default
#
# Default adding cert_defaultKeyStore
#       You can overwrite these in Podman, Docker or OpenShift
ENV cert_defaultKeyStore=/etc/ssl/certs/ca-certificates.crt

COPY --chown=default:0 /src/main/liberty/config /config

#-> this does not work if you don't have internet access
# You may want to use a different liberty image in that case
RUN features.sh

COPY --chown=default:0 target/*.war /config/apps

RUN configure.sh

The default user has id 1001.

Note that you don’t really need to put the cert_defaultKeyStore environment variable in the Container image, you can also add this at runtime .

For example, to add the environment variable cert_defaultKeyStore using Podman, use --env :

podman run -d --replace --name mpjwtpublic -p 9081:9081 --add-host isam.tombosmans.eu:10.20.30.40 
 --env 'cert_defaultKeyStore=/etc/ssl/certs/ca-certificates.crt' 
 mpjwtpublic:v1.0.0

Build

To build the application, and package it in a Container, you need to first package the application (using Maven):

cd <your-directory>/isam-liberty-jwt/mpJwt_demo
mvn package

Then you can build and run the Container:

podman build -t mpjwtpublic:v1.0.0 .
podman run -d --replace --name mpjwtpublic -p 9080:9080 -p 9443:9443 --add-host isam.tombosmans.eu:10.168.73.248 mpjwtpublic:v1.0.0

Note the --add-host to add an entry to the container’s /etc/hosts file, for the jwksUri resolution further down. This is entirely optional and obviously not necessary if your dns is working correctly.

To make the container publicly available (for instance, to deploy to OpenShift), you can publish the container to a repository of your choice (eg. quay.io or a private repository):

podman login <repo>
podman push <repo>/mpjwtpublic:v1.0.0

Liberty

Server.xml

Add the feature microProfile-5.0 feature:

<featureManager>
    <feature>microProfile-5.0</feature>
</featureManager>

It contains a bunch of stuff, among which mpJwt-2.0.

Enable the ports

<httpEndpoint id="defaultHttpEndpoint"
  host="*"
  httpPort="9080"
  httpsPort="9443" />

Configure the mpJwt feature

In older version of the mpJwt feature, you needed to configure it in the microprofile-config.properties file in the webapp itself. Today, all of the configuration can be done in the server.xml directly (if you’d like).

Some of the configuration is exclusively available in the server.xml, by the way. The jwksUri is not an option in the microprofile-config.properties.

This is a very simple configuration:

<mpJwt id="testJWT"
  issuer="https://issuer"
  jwksUri="https://isam.tombosmans.eu:444/jwks.json"
  tokenHeader="Authorization"
  userNameAttribute="sub"
/>
  • jwksUri: this points to the jwks endpoint on WebSeal
  • issuer: the issuer needs to match the iss claim
  • userNameAttribute: changes the default upn of the mpJwt profile to the default in oidc sub

WebSeal

General configuration

I’ve just added a user the local repository , so I’m able to login.

Junction

This junction is a transparent path junction to the “/api” application.

Image:API Junction

The server to connect to, obviously depends on your backend. Here, this is a local Podman installation, connecting on the http port.

Image:API Junction

With OpenShift, this would the Route url to your application, or the url for the Ingress you’ve configured (for instance, Istio Service Mesh).

jwt configuration

This goes in the webseald.conf for the reverse proxy

[jwt:/api]
key-label = jwtsign
claim = text::https://issuer::iss
claim = attr::AZN_CRED_PRINCIPAL_NAME::sub
claim = attr::AZN_CRED_PRINCIPAL_NAME::upn
claim = attr::AZN_*
claim = text::[Echoer]::roles
claim = text::[Echoer,Tester,group1,group2]::groups
include-empty-claims = false
hdr-name = Authorization
hdr-format = Bearer %TOKEN%
lifetime = 0

The groups claim needs to contain the roles that can be used with the @RolesAllowed annotation. Notice the syntax, you must enclose multiple values in square brackets.

jwks endpoint

The same jwks application is enabled as for the openidconnectclient configuration.

Note that I use the same certificate (jwtsign) in both cases.

Test application

RolesEndpoint

This endpoint is directly taken from the original sample and merely updated to work with microProfile 5.0.

ClaimsEndpoint

This piece of code includes 2 ways to retrieve Claim values:

  • through Injection of the JsonWebToken, which allows access to the raw jwt token as well as the claims
  • through Injection of the individual Claims. Examples below are of a standard jwt claim (“iss”), and of a custom claim (“AZN_CRED_BROWSER_INFO”), added by WebSeal’s jwt junction.

The user needs to be part of the Echoers role to access the /api/claims url (this is accomplished by the groups claim added in WebSeal’s jwt configuration)

package dev.microprofile.jwt;

import java.security.Principal;

import jakarta.annotation.security.RolesAllowed;
import jakarta.enterprise.context.RequestScoped;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.SecurityContext;

// CLAIM
import jakarta.inject.Inject;
import org.eclipse.microprofile.jwt.*;
import jakarta.json.JsonArray;

@RequestScoped
@Path("claims")
public class ClaimsEndpoint {
    @Inject
    private JsonWebToken callerPrincipal;

    @Inject
    @Claim("iss")
    private String issuer;

    @Inject
    @Claim("AZN_CRED_BROWSER_INFO")
    private String claimBrowserInfo;

    @Inject
    @Claim("AZN_CRED_REGISTRY_ID")
    private String claimRegistryId;

    @GET
    @Path("test")

    public String getTest() {
        return "issuer: " + issuer + "\n<br/>browser:" + claimBrowserInfo + "\n<br/>registry id:" + claimRegistryId;
    }
    @GET
    @RolesAllowed("Echoer")

    public String echoInput(@Context SecurityContext sec, @QueryParam("input") String input) {
        Principal user = sec.getUserPrincipal();
        System.out.println("Issuer " + issuer);
        if (callerPrincipal == null) {
          return "user="+user.getName();
        }
        return "user="+user.getName() + "\n" + callerPrincipal.getRawToken() + "\n<br/>claim: " + callerPrincipal.getClaim("AZN_CRED_PRINCIPAL_UUID");
    }
}

Login to WebSeal

So to access the url’s, you’ll have to login to ISAM first.

Image:API Junction

/api/claims

Then you can access the endpoints.

Image:API Claims

Notice the full jwt claim displayed (from the JsonWebToken object), as well as the issuer value and the value of the AZN_CRED_PRINCIPAL_UUID WebSeal claim, added by .getClaim().

/api/claims/test

This endpoint uses the @Claim(“AZN_CRED_BROWSER_INFO”) Annotation to access the claim info.

Image:API Claims

Open Liberty

To enable tracing of the jwt consumption process on Liberty, you can add this to the server.xml.

<logging traceSpecification="*=audit:com.ibm.ws.security.jwt.*=finer"/>

You can also enable this at runtime by creating a file at /config/configDropins/overrides/tracing.xml

<server>
<logging traceSpecification="*=audit:com.ibm.ws.security.jwt.*=finer"/>
</server>

Maven

To run the samples locally, you need Maven installed. Maven allows you to run in development mode, so changes to code or configuration are immediately visible.

This is all pretty easy and explained on the Open Liberty website.

Additional Links