IBM Support

How to use JWTs with JAX-WS WS-Security in WebSphere Application Server traditional

How To


Summary

You can use JWTs with JAX-WS WS-Security in WebSphere Application Server traditional by using custom token generator, token consumer, and caller login modules.

Objective

OpenID Connect (OIDC) and JWT authentication (JWT SSO) are frequently used as authentication mechanisms with WebSphere Application Server.  When a user is logged in by using one of these methods, the information for the user is contained within the JWT and optionally an idToken.  An administrator might want to authenticate to a down-stream web service by using the user's JWTs because other mechanisms present problems for their use case.
The first process that an administrator might think to choose to propagate a user's identity for WS-Security is an LTPAv2 token.  This approach works if the administrator needs to use only authentication for the user.  However, if the target application requires the user's credentials on the runAs Subject, unless the user is present in the WebSphere registry, an LTPAv2 token does not work.  
When a user is logged in to WebSphere by using OIDC or JWT SSO, the user is frequently not in the WebSphere registry. Therefore, unless only authentication is required, the LTPAv2 token approach is not appropriate. The best solution is to send the JWT and idToken to the target service, locally validate the JWT, then perform an identity assertion login that does not involve the WebSphere registry. The steps that the custom code performs to log in is similar to what the WebSphere OIDC trust association interceptor (TAI) does.

Environment

The example in this document assumes that the source of the user's original authentication to WebSphere is an OIDC path that yields both a JWT access token and an idToken.  The JWT and idToken are sent to a target JAX-WS provider by using a JAX-WS client.  Both the JAX-WS client and provider are configured by using JAX-WS policy and bindings.  Custom JAAS login modules are used on both the client and provider to emit and consume the JWTs.

Outline

This procedure builds on the steps in the Creating custom security tokens for Generating and Consuming custom tokens with the Generic Issue Login Modules task in IBM Docs.  To reach the objective in this document, you run through most of that task, then make modifications to the settings, and use different code:
  1. Client token generator custom code:
    1. Get the JWT access token and idToken Strings from the runAs Subject
    2. Create a SOAPElement that includes both Strings
  2. Provider token consumer custom code:
    1. Get the JWT access token and idToken Strings from the inbound SOAPElement
    2. Verify the JWT
  3. Provider caller login module custom code:
    1. Get the identity from the idToken
    2. Set up the Subject for identity assertion

Security Considerations

  • When a user logs in with OIDC, you have access to both the user's access token and idToken.  The access token might, or might not be a JWT.  If you have both a JWT and idToken, send the tokens then validate the JWT at the receiver side.  This process establishes trust for the idToken and makes sure that you are not accepting just any idToken.
  • You do not need to sign a JWT in a SOAP message; like the LTPAv2 token, the token itself contains a signature. 
  • Depending on your use case, consider signing the idToken in the SOAP message.
  • When you validate a JWT, you can use the APIs that are available in the OidcClientHelper class.  There are two verifyJwt methods available in this class that validate the token by using discovery where there is no other configuration required on the application server. Although it might be tempting to use these configuration-free methods, they are not secure because you accept any JWT issued by anyone. 
    • For instance, you might want to accept only JWTs that are issued by your Azure tenant.  However, since you allowed your service to validate JWTs by using discovery, you inadvertently allowed Google users also access to your service.
    • The most secure method is to use the verifyJwt methods that require OIDC TAI configuration.  These methods make sure that the runtime accepts only JWTs that you configured it to allow. 
    • You can be as secure or lax as you choose with the OidcClientHelper APIs.

Steps

image-20230927185006-1 For the sake of brevity and readability, none of the code in this document contains logging or error handling, such as the protection from NPEs.
image-20230927185006-1 The code that is in this example might appear complicated, especially if you are not familiar with JAAS login modules.  However, much of the work that is done in each class in this example is standard and is included so that you have a complete class file that compiles.  The part of the code that you need to focus on is identified with image-20230928114718-1 in each class file.

image-20230927185006-1 You can package your custom code with your application or in a separate JAR file.  Some reasons why you might want to package the classes in a JAR file are: 1) you cannot modify your service because it is a commercial application, 2) applications share the JAAS login modules, and 3) you don't want to redeploy your application each time you change a JAAS login module.
image-20230927185006-1 You can change the values for the namespace and local name of your SOAP elements.  However, if you the values, you need to make sure that you also change the values when you are working with your policy and bindings.  When you perform the steps in this sample, use the http://www.acme.com namespace and MyToken local name because they are the names that are used in the companion task
  1. Create three JAAS login modules and package them either with your application or in a JAR file.
    • Generator JAAS login module
      package test.tokens;
      
      import java.util.ArrayList;
      import java.util.HashMap;
      import java.util.Map;
      
      import javax.security.auth.Subject;
      import javax.security.auth.callback.CallbackHandler;
      import javax.security.auth.login.LoginException;
      import javax.security.auth.spi.LoginModule;
      import javax.xml.soap.SOAPFactory;
      import javax.xml.soap.SOAPElement;
      import javax.xml.namespace.QName;
      
      import com.ibm.websphere.wssecurity.wssapi.token.GenericSecurityTokenFactory;
      import com.ibm.websphere.wssecurity.wssapi.token.SecurityToken;
      import com.ibm.websphere.security.oidc.util.OidcClientHelper;
      
      public class MyCustomGenerator implements LoginModule {
      
        public static final String LOCALNAME = "MyToken";
        public static final String NAMESPACE = "http://www.acme.com";
        public static final String PREFIX = "acme";
        public static final String IDTOKEN_LOCALNAME = "idToken";
        public static final String ACCESSTOKEN_LOCALNAME = "jwt";
      
        public static final QName VALUE_TYPE = new QName(NAMESPACE, LOCALNAME);
        public static final QName IDTOKEN_QNAME = new QName(NAMESPACE, IDTOKEN_LOCALNAME);
        public static final QName ACCESSTOKEN_QNAME = new QName(NAMESPACE, IDTOKEN_LOCALNAME);
      
        private Map _sharedState;
        private Map _options;
        private CallbackHandler _handler;
      
        public void initialize(Subject subject, CallbackHandler callbackHandler,
                               Map<String, ?> sharedState, Map<String, ?> options) {
      
          this._handler = callbackHandler;
          this._sharedState = sharedState;
          this._options = options;  
        }
        public boolean login() throws LoginException {
      
          //you have access to the custom properties that are provided
          //on the configured callback handler
          PropertyCallback propertyCallback = new PropertyCallback(null);
          Callback[] callbacks = new Callback[] { propertyCallback};
      
          GenericSecurityTokenFactory factory = null;
      
          try {
            this._handler.handle(callbacks);
      
            factory = GenericSecurityTokenFactory.getInstance();
            SOAPElement tokenElement = createCustomElement(factory);  //<== YOUR CUSTOM CODE
            SecurityToken myToken = factory.getToken(tokenElement, VALUE_TYPE);
      
            //Put the token in a list on the shared state where it will be available to be used by
            //stacked login modules
            factory.putGeneratorTokenToSharedState(_sharedState, myToken);
          } catch (Exception e) {
            LoginException le = new LoginException(e.toString());
            le.initCause(e);
          }
          return true;
        }
      
        private SOAPElement createCustomElement(GenericSecurityTokenFactory gstFactory) throws Exception {
      /*
        <acme:MyToken xmlns:acme="http://www.acme.com" 
              xmlns:utl="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" utl:Id="cust_3">
          <acme:idToken>BINARY</acme:idToken>
          <acme:jwt>BINARY/acme:jwt>
        </acme:MyToken>
      */
      
          SOAPFactory factory = SOAPFactory.newInstance();
      
          //Create the MyToken element
          SOAPElement tokElement = factory.createElement(LOCALNAME, PREFIX, NAMESPACE);
          //Add the Id attribute
          tokElement.addAttribute(new QName("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd", "Id", "utl"), gstFactory.createUniqueId());
      
          //add the idtoken
          {
            String idToken = OidcClientHelper.getIdTokenFromSubject();
      
            //Create the idToken element
            SOAPElement element = factory.createElement(IDTOKEN_LOCALNAME, PREFIX, NAMESPACE);
            element.addTextNode(idToken);
      
            //Add the element to myToken
            tokElement.addChildElement(element);
          }
          //add the jwt
          {
            String accessToken = OidcClientHelper.getAccessTokenFromSubject();
      
            //Create the accessToken element
            SOAPElement element = factory.createElement(ACCESSTOKEN_LOCALNAME, PREFIX, NAMESPACE);
            element.addTextNode(accessToken);
      
            //Add the element to myToken
            tokElement.addChildElement(element);
          }
          return tokElement;
        }
        public boolean logout() throws LoginException {
          return false;
        } 
        public boolean abort() throws LoginException {
          return false;
        }
        public boolean commit() throws LoginException {
          return true;
        }
      }
    • Consumer JAAS login module
      package test.tokens;
      
      import java.util.Map;
      
      import org.apache.axiom.om.OMElement;
      
      import javax.xml.namespace.QName;
      import javax.security.auth.Subject;
      import javax.security.auth.callback.Callback;
      import javax.security.auth.callback.CallbackHandler;
      import javax.security.auth.login.LoginException;
      import javax.security.auth.spi.LoginModule;
      
      import com.ibm.websphere.wssecurity.callbackhandler.PropertyCallback;
      import com.ibm.websphere.wssecurity.wssapi.token.GenericSecurityTokenFactory;
      import com.ibm.websphere.wssecurity.wssapi.token.SecurityToken;
      import com.ibm.wsspi.wssecurity.wssapi.OMStructure;
      import com.ibm.websphere.security.oidc.util.OidcClientHelper;
      import com.ibm.websphere.wssecurity.wssapi.WSSUtilFactory;
      
      public class MyCustomConsumer implements LoginModule {
      
        private CallbackHandler _handler;
        private Map _sharedState;
      
        public void initialize(Subject subject, CallbackHandler callbackHandler,
                               Map<String, ?> sharedState, Map<String, ?> options) {
          this._handler = callbackHandler;
          this._sharedState = sharedState;
      
          return;
        }
      
        public boolean login() throws LoginException {
      
          //you have access to the custom properties that are provided
          //on the configured callback handler
          PropertyCallback propertyCallback = new PropertyCallback(null);
          Callback[] callbacks = new Callback[] { propertyCallback};
      
          boolean isValid = false;
      
          try {
            this._handler.handle(callbacks);
            GenericSecurityTokenFactory factory = GenericSecurityTokenFactory.getInstance();
      
            //Get the token that was consumed by the GenericIssuedConsumeLoginModule
            SecurityToken myToken = factory.getConsumerTokenFromSharedState(_sharedState, MyCustomToken.VALUE_TYPE); 
      
            //Get the token's element
            Object obj = myToken.getXML();
            OMElement tokenElement = ((OMStructure)obj).getNode();
      
            isValid = verifyCustomToken(tokenElement); //<== YOUR CUSTOM CODE
          } catch (Exception e) {
            LoginException le = new LoginException(e.getMessage());
            le.initCause(e);
            throw le;
          }
          return isValid;
        }
        public boolean commit() throws LoginException {
          return true;
        }
        public boolean logout() throws LoginException {
          return false;
        }
        public boolean abort() throws LoginException {
          return false;
        }
        boolean verifyCustomToken(OMElement tokenElement) {
            /*
              <acme:MyToken xmlns:acme="http://www.acme.com" 
                    xmlns:utl="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" utl:Id="cust_3">
                <acme:idToken>BINARY</acme:idToken>
                <acme:jwt>jBINARY/acme:jwt>
              </acme:MyToken>
            */
            //You don't need to be concerned abount the binary values of the idToken
            //and jwt.  The jose4j libraries that are in the OIDC runtime handles
            //everything for you.
      
            //get the access token
            String accesstoken = getAccessToken(tokenElement);
      
            //verify the access token
            boolean isValid = OidcClientHelper.verifyJwt(accesstoken);
      
            //if you need to validate additional information from the idtoken or jwt, you can get a
            //Map of the claims from the tokens by using the OidcClientHelper.json2map(String)
            //method
            
            return isValid;
        }
        //these methods allow convenient access to the tokens from
        //an OMElement or SecurityToken
        public static String getElement(OMElement om, QName type) throws Exception {
          OMElement el = om.getFirstChildWithName(type);
          return el.getText();
        }
        public static String getElement(SecurityToken token, QName type) throws Exception {
          return getElement(getOmElement(token),type);
        }
        public static String getIdToken(OMElement om) throws Exception {
          return getElement(om,MyCustomGenerator.IDTOKEN_QNAME);
        }
        public static String getIdToken(SecurityToken token) throws Exception {
          return getElement(token,MyCustomGenerator.IDTOKEN_QNAME);
        }
        public static String getAccessToken(OMElement om) throws Exception {
          return getElement(om,MyCustomGenerator.ACCESSTOKEN_QNAME);
        }
        public static String getAccessToken(SecurityToken token) throws Exception {
          return getElement(token,MyCustomGenerator.ACCESSTOKEN_QNAME);
        }
        public static OMElement getOmElement(SecurityToken token) {
          return((com.ibm.wsspi.wssecurity.wssapi.OMStructure)token.getXML()).getNode();  
        }
      }
    • image-20230928151114-1 (Optional) Caller JAAS login module
      If you need to have the identity from your JWT or idToken on the runAs Subject, create a caller login module.
      package test.tokens;
      
      import java.util.ArrayList;
      import java.util.Collection;
      import java.util.Hashtable;
      import java.util.Iterator;
      import java.util.Map;
      
      import javax.security.auth.Subject;
      import javax.security.auth.callback.CallbackHandler;
      import javax.security.auth.login.LoginException;
      import javax.security.auth.spi.LoginModule;
      
      import com.ibm.wsspi.security.token.AttributeNameConstants;
      import com.ibm.websphere.wssecurity.wssapi.token.SecurityToken;
      import com.ibm.websphere.wssecurity.wssapi.token.GenericSecurityToken;
      import com.ibm.websphere.wssecurity.wssapi.token.SAMLToken;
      
      import com.ibm.websphere.security.oidc.util.OidcClientHelper;
      
      public class MyCustomCaller implements LoginModule {
        private Subject _subject;
        private CallbackHandler _handler;
        private Map _sharedState;
        private Map _options;
      
        private String _idtoken = null;
        private String _accesstoken = null;
        private ArrayList<String> _groups = null;
        private String _principal = null;
      
        @Override
        public void initialize(Subject subject, CallbackHandler callbackHandler,
                               Map<String, ?> sharedState, Map<String, ?> options) {
      
          this._subject = subject;
          this._handler = callbackHandler;
          this._sharedState = sharedState;
          this._options = options;
        }
      
        @Override
        public boolean login() throws LoginException {
      
          // Checks whether or not caller identification is finished.
          Boolean finishProcess = (Boolean) this._sharedState
                                  .get(com.ibm.wsspi.wssecurity.core.Constants.WSSECURITY_CALLER_PROCESS_DONE);
          try {
            //although it might not technically be needed to do a lot of this code for a caller,
            //if your JAAS login module might be in a login stack with another built-in caller login
            //modules, you need this code in order to interact with the other login modules and keep
            //them from overwriting your work and vice-versa
            if ((finishProcess == null) || !finishProcess.booleanValue()) {
      
              // Get the caller identities
              java.util.Collection<SecurityToken> callerIdentityCandidates = 
                    (Collection<SecurityToken>) this._sharedState.get(                                                                                                                        
                     com.ibm.wsspi.wssecurity.core.Constants.WSSECURITY_CALLER_IDENTITY_CANDIDATES);
      
              // If there are caller identities, process them
              if (callerIdentityCandidates != null && callerIdentityCandidates.size() > 0) {
                Iterator<SecurityToken> it = callerIdentityCandidates.iterator(); 
                while (it.hasNext()) {
                  SecurityToken token = it.next();
      
                  //your custom token is a GenericSecurityToken, but so is a SAMLToken
                  if ((token instanceof GenericSecurityToken) &&
                      !(token instanceof SAMLToken)) {
      
                    //make extra sure the GenericSecurityToken is yours
                    if (MyCustomGenerator.VALUE_TYPE.equals(token.getValueType())) {
                         Hashtable<String,Object> map = getCustomCallerMap(token); //<== YOUR CUSTOM CODE
      
                         if (map!=null) {
                             _sharedState.put(AttributeNameConstants.WSCREDENTIAL_PROPERTIES_KEY,map);
                         }
                        //identify your token as the final caller, then tell the other caller login modules
                        //that they don't need to do anything
                        this._sharedState.put(com.ibm.wsspi.wssecurity.core.Constants.WSSECURITY_CALLER_IDENTITY, token);
                        this._sharedState.put(com.ibm.wsspi.wssecurity.core.Constants.WSSECURITY_CALLER_PROCESS_DONE, Boolean.TRUE);
                    }
                    break;
                  }
                }
              }
            }
          } catch (Exception e) {
            e.printStackTrace();                            
          }
          return(true);
        }
        private token getCustomCallerMap(SecurityToken token) {
            //get your data from the OMElement in the token
            getCustomData(token);     
      
            //==> Now we're starting the identity assertion login
            //by using a hashmap on the private credentials
            // 
            Hashtable<String,Object> map = new Hashtable<String,Object>();
      
            map.put(AttributeNameConstants.WSCREDENTIAL_SECURITYNAME,_principal);
      
            //this is technically a workaround.  there is no way to pass the princiapl back
            //from a custom consumer to the GenericIssuedConsumeLoginModule
            ((com.ibm.ws.wssecurity.wssapi.token.impl.SecurityTokenImpl)token).setPrincipal(_principal); 
      
            //when you create your uniqueid, you can get the realm from the token, 
            //or use the default realm.  Using the default realm can prevent processing 
            //problems later
            String uid = "user:" + getDefaultRealm() + "/"  + _principal;
            map.put(AttributeNameConstants.WSCREDENTIAL_UNIQUEID, uid);
      
            //if you have groups, set them on the Subject
            if (_groups!=null) {
              if (_groups.size() != 0) {
                map.put(AttributeNameConstants.WSCREDENTIAL_GROUPS,_groups);
              }
            }
            //put the idtoken and access token in the map on the creds
            //you can chhose any key you like for each
            map.put("access_token",_accesstoken);
            map.put("id_token",_idtoken);                               
      
            return map;
        }
        private void getCustomData(SecurityToken token) throws Exception {
      
          _idtoken = MyCustomConsumer.getIdToken(token);
          _accesstoken = MyCustomConsumer.getAccessToken(token);
      
          //you need this information from the idToken to set up the Subject
          //although you can access this information in the token consumer, you cannot
          //set the information on the final Subject from the token consumer
          if (_idtoken!=null) {
            java.util.Map<String,Object> claims = OidcClientHelper.getJwtClaimsAsMap(_idtoken);
            _principal = (String)claims.get("sub");
            _groups = (ArrayList<String>)claims.get("groupIds");
          }
        }
        private String getDefaultRealm() {
          String realm = null;
          com.ibm.ws.security.core.ContextManager ctx = com.ibm.ws.security.core.ContextManagerFactory.getInstance();
          if (ctx != null) {
            realm = ctx.getDefaultRealm();
          }
          return realm;
        }
        public boolean logout() throws LoginException {
          return false;
        }
        public boolean abort() throws LoginException {
          return false;
        }
        public boolean commit() throws LoginException {
          return false;
        }
      }
  2. image-20230928151114-1 (Optional) If you packaged your login modules in a JAR file instead of in your application, put the JAR file in the (WAS_HOME)/lib/ext directory on both the client and the provider.
  3. Perform the steps on  Generating and Consuming custom tokens with the Generic Issue Login Modules task in IBM Docs, starting with:
    • step 3: Create new JAAS login configurations.
    • Do not perform the last two steps to restart the server and test your application.
  4. image-20230928151114-1 (Optional) If you need to have the identity from your JWT or idToken on the runAs Subject, add a JAAS login caller configuration 
    1. In the administrative console, click Security > Global security
    2. Under Authentication, click Java Authentication and Authorization Service
    3. Click System logins
    4. Create the caller JAAS login configuration
      The minimum requirements for a WS-Security caller JAAS login configuration are:
      com.ibm.ws.wssecurity.impl.auth.module.PreCallerLoginModule
      (tokenLoginModule)
      com.ibm.ws.wssecurity.impl.auth.module.WSWSSLoginModule
      com.ibm.ws.security.server.lm.ltpaLoginModule
      com.ibm.ws.security.server.lm.wsMapDefaultInboundLoginModule
      In this step, in addition to your custom token caller login module, there are the login modules for all the built-in tokens.  This procedure allows you to use one caller configuration in any WS-Security provider binding.  It also makes sure that your caller configuration can handle a policy that has more than one caller candidate.
      1. Click New
        • Specify Alias = test.caller.custom
      2. Click Apply
      3. For each of the login class names in the box, perform the following actions:
        1. Click New
        2. Specify Module class name = (class name)
        3. Click OK 
          com.ibm.ws.wssecurity.impl.auth.module.PreCallerLoginModule
          test.tokens.MyCustomCaller
          com.ibm.ws.wssecurity.impl.auth.module.UNTCallerLoginModule
          com.ibm.ws.wssecurity.impl.auth.module.X509CallerLoginModule
          com.ibm.ws.wssecurity.impl.auth.module.LTPACallerLoginModule
          com.ibm.ws.wssecurity.impl.auth.module.LTPAPropagationCallerLoginModule
          com.ibm.ws.wssecurity.impl.auth.module.KRBCallerLoginModule
          com.ibm.ws.wssecurity.impl.auth.module.SAMLCallerLoginModule
          com.ibm.ws.wssecurity.impl.auth.module.GenericIssuedTokenCallerLoginModule
          com.ibm.ws.wssecurity.impl.auth.module.WSWSSLoginModule
          com.ibm.ws.security.server.lm.ltpaLoginModule
          com.ibm.ws.security.server.lm.wsMapDefaultInboundLoginModule
      4. Click OK
    5. Click Save
  5. image-20230928151114-1 (Optional) Configure the provider binding to add the new caller configuration
    1. Click Services > Service providers
    2. Click your service provider
    3. Click customTokenProviderBinding > WS-Security > Caller
    4. Click New, then specify the following values:
      • Name = myCustomCaller
      • Caller identity local part = http://www.acme.com
      • Caller identity namespace URI = MyToken
    5. Under JAAS login, choose test.caller.custom
    6. Click Save
  6. Restart the application server to apply the JAAS configuration changes
  7. Test your service.

Example

The following example illustrates the SOAP Security header that is produced when you follow the preceding procedure.
<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" soapenv:mustUnderstand="1">
    <acme:MyToken xmlns:acme="http://www.acme.com" xmlns:utl="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" utl:Id="cust_3">
        <acme:idToken>BINARY_DATA</acme:idToken>
        <acme:jwt>BINARY_DATA</acme:jwt>
    </acme:MyToken>
</wsse:Security>

Document Location

Worldwide

[{"Type":"MASTER","Line of Business":{"code":"LOB45","label":"Automation"},"Business Unit":{"code":"BU059","label":"IBM Software w\/o TPS"},"Product":{"code":"SSEQTP","label":"WebSphere Application Server"},"ARM Category":[{"code":"a8m50000000CdOjAAK","label":"WebSphere Application Server traditional-All Platforms-\u003ESecurity-\u003EApplication Security"}],"ARM Case Number":"","Platform":[{"code":"PF025","label":"Platform Independent"}],"Version":"All Versions"}]

Document Information

Modified date:
28 September 2023

UID

ibm17041535