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
Environment
Outline
- Client token generator custom code:
- Get the JWT access token and idToken Strings from the runAs Subject
- Create a SOAPElement that includes both Strings
- Provider token consumer custom code:
- Get the JWT access token and idToken Strings from the inbound SOAPElement
- Verify the JWT
- Provider caller login module custom code:
- Get the identity from the idToken
- 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](/support/pages/system/files/inline-images/image-20230927185006-1.png)
![image-20230927185006-1](/support/pages/system/files/inline-images/image-20230927185006-1.png)
![image-20230928114718-1](/support/pages/system/files/inline-images/image-20230928114718-1.png)
![image-20230927185006-1](/support/pages/system/files/inline-images/image-20230927185006-1.png)
![image-20230927185006-1](/support/pages/system/files/inline-images/image-20230927185006-1.png)
- 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(); } }
(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; } }
- Generator JAAS login module
(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.
- 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.
(Optional) If you need to have the identity from your JWT or idToken on the runAs Subject, add a JAAS login caller configuration
- In the administrative console, click Security > Global security
- Under Authentication, click Java Authentication and Authorization Service
- Click System logins
- Create the caller JAAS login configuration
The minimum requirements for a WS-Security caller JAAS login configuration are:
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.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- Click New
- Specify Alias = test.caller.custom
- Click Apply
- For each of the login class names in the box, perform the following actions:
- Click New
- Specify Module class name = (class name)
- 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
- Click OK
- Click New
- Click Save
(Optional) Configure the provider binding to add the new caller configuration
- Click Services > Service providers
- Click your service provider
- Click customTokenProviderBinding > WS-Security > Caller
- Click New, then specify the following values:
- Name = myCustomCaller
- Caller identity local part = http://www.acme.com
- Caller identity namespace URI = MyToken
- Under JAAS login, choose test.caller.custom
- Click Save
- Restart the application server to apply the JAAS configuration changes
- Test your service.
Example
<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>
Related Information
Generating and Consuming custom tokens with the Generic Issue Login Modules
Creating custom security tokens for web services security by using the GenericS…
Configuring a UsernameToken caller configuration with no registry interaction
WebSphere WS-Security Examples : JAX-WS Policy/Binding Configuration and Code S…
Document Location
Worldwide
Was this topic helpful?
Document Information
Modified date:
28 September 2023
UID
ibm17041535