Saturday, January 20. 2018
Certificates and WS-Security with JavaEE integration in Wildfly
This the fourth and last entry about certificate login in Wildfly for a jax-ws application. The first pair of posts were dedicated to TLS mutual authentication (the client certificate presented at TLS level was then used to identify the user inside JavaEE). The previous entry introduced WS-Security, the SOAP protocol is extended to use headers that can add different security features. Using WSS the message is signed and the X509 certificate is included in the request message as a binary token. In this last entry my intention was just using that certificate in the header and login the user with it. But I had problems implementing this last step and that was the reason to separate this topic in a new entry. I am going to explain the steps to finally make it work one by one.
By default wildfly has an interceptor that can (out of the box) log a user in with a username/password token. This interceptor class is SubjectCreatingPolicyInterceptor which obtains the token, gets the username and password inside it and validates the pair against the security domain configured in the endpoint. But the entry's goal is different, we are using a certificate token (X509), and there is no interceptor for that by default. Therefore I decided to develop a CertificateBinaryTokenInterceptor.
My class has a method to obtain the certificate from the message. The WSS4J implementation stores all the validations executed over the request in the message itself. The signature verification can be retrieved from those validations and the certificate is just part of the validation results. The method iterates over the list of verifications and gets the one that is a signature, is validated (signature has been checked) and has a certificate attached.
private X509Certificate getCertificate(SoapMessage message) {
List handlerResults = (List) message.get(WSHandlerConstants.RECV_RESULTS);
if (handlerResults != null) {
for (WSHandlerResult handlerResult : handlerResults) {
List engResults = handlerResult.getResults();
if (engResults != null) {
for (WSSecurityEngineResult engResult : engResults) {
Boolean validated = (Boolean) engResult.get(WSSecurityEngineResult.TAG_VALIDATED_TOKEN);
X509Certificate cert = (X509Certificate) engResult.get(WSSecurityEngineResult.TAG_X509_CERTIFICATE);
Integer action = (Integer) engResult.get(WSSecurityEngineResult.TAG_ACTION);
if (Boolean.TRUE.equals(validated) && cert != null && action != null && WSConstants.SIGN == action) {
return cert;
}
}
}
}
}
return null;
}
The main method uses the certificate to log the user as it is done in the default SubjectCreatingPolicyInterceptor.
@Override
public void handleMessage(SoapMessage message) throws Fault {
Endpoint ep = message.getExchange().get(Endpoint.class);
X509Certificate cert = getCertificate(message);
SecurityContext context = message.get(SecurityContext.class);
if (cert == null || context == null || context.getUserPrincipal() == null) {
// no certificate, do nothing
super.handleMessage(message);
} else {
// certificate found => perform a login with it
// the principal name is going to be as in the users.jks in lowercase
Principal principal = new SimplePrincipal(cert.getSubjectX500Principal().getName().toLowerCase());
Subject subject = new Subject();
if (ep.getSecurityDomainContext().isValid(principal, cert, subject)) {
ep.getSecurityDomainContext().pushSubjectContext(subject, principal, cert);
SecurityContext sc = new DefaultSecurityContext(principal, subject);
message.put(SecurityContext.class, sc);
} else {
throw MESSAGES.authenticationFailed(principal.getName());
}
}
}
Then the interceptor is configured in the Echo service:
@Stateless
@WebService(name = "echo",
targetNamespace = "http://es.rickyepoderi.sample/ws",
serviceName = "echo-service")
@Policy(placement = Policy.Placement.BINDING, uri = "WssX509V3Token10.xml")
@DeclareRoles("Users")
@SOAPBinding(style = SOAPBinding.Style.RPC)
@EndpointConfig(configFile = "WEB-INF/jaxws-endpoint-config.xml", configName = "Custom WS-Security Endpoint")
@InInterceptors(interceptors = {
"es.rickyepoderi.sample.CertificateBinaryTokenInterceptor"
})
public class Echo {
Finally the security context is associated to the application (the same security domain used before) but using the jboss-ejb3.xml. This is now necessary, if the configuration was setup in the web.xml/jboss-web.xml files it would not work, the certificate would be searched at TLS level and now it goes in the WSS SOAP header. The file just assigns the security domain to all EJBs:
<?xml version="1.1" encoding="UTF-8"?>
<jboss:ejb-jar xmlns:jboss="http://www.jboss.com/xml/ns/javaee"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:s="urn:security:1.1"
xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee http://www.jboss.org/j2ee/schema/jboss-ejb3-2_0.xsd http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/ejb-jar_3_1.xsd"
version="3.1"
impl-version="2.0">
<assembly-descriptor>
<s:security>
<ejb-name>*</ejb-name>
<s:security-domain>certificate-sec-domain</s:security-domain>
</s:security>
</assembly-descriptor>
</jboss:ejb-jar>
I was pretty sure about this solution but...
Interceptor for {http://es.rickyepoderi.sample/ws}echo-service has thrown exception, unwinding now: java.lang.IllegalArgumentException: only string password accepted
at org.jboss.as.webservices.security.ElytronSecurityDomainContextImpl.isValid(ElytronSecurityDomainContextImpl.java:71)
at es.rickyepoderi.sample.CertificateBinaryTokenInterceptor.handleMessage(CertificateBinaryTokenInterceptor.java:98)
at es.rickyepoderi.sample.CertificateBinaryTokenInterceptor.handleMessage(CertificateBinaryTokenInterceptor.java:32)
at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:308)
at org.apache.cxf.transport.ChainInitiationObserver.onMessage(ChainInitiationObserver.java:121)
...
It seems that the current integration between webservices and elytron subsystem just admits password logins. Looking the code this restriction is pretty clear. Elytron admits more types of login but, for the moment, the web-services just let you login using a password.
I know that in the previous security subsystem this type of login is admitted, so I just reconfigure the wildfly server to use a previous security-domain with a certificate login module.
/subsystem=security/security-domain=certificate-domain:add(cache-type=default)
/subsystem=security/security-domain=certificate-domain/authentication=classic:add()
/subsystem=security/security-domain=certificate-domain/authentication=classic/login-module=Certificate:add(code=Certificate, flag=required)
/subsystem=security/security-domain=certificate-domain/authentication=classic/login-module=Certificate:write-attribute(name=module-options, value={password-stacking=useFirstPass, securityDomain=certificate-domain})
/subsystem=security/security-domain=certificate-domain/jsse=classic:add(keystore={url="${jboss.server.config.dir}/users.jks", password=Kiosko_00})
Just the certificate module is used (stacking can be used to add roles from other modules). The user will be logged in without any role to the application.
Then I changed the jboss-ejb3.xml to use the new certificate-domain instead of the previous elytron realm. And it simply works. As I remembered, you can make a login with the certificate just passing its X509 representation as the password object. The login module just picks it up and checks if it is in the users.jks key-store. Here it is the video, as you see the current user is again the subject of the certificate.
In this last entry of the certificate security for jax-ws web services series, the application uses a WS-Security X509 token to execute a login inside an interceptor. The solution is very similar to the one that default wildfly uses for username tokens (the documented SubjectCreatingPolicyInterceptor class) but with a certificate. Nevertheless the new elytron susbsystem cannot be used but previous security domain works without any flaw. I think that elytron is still very new and it needs some improvements in these integration areas (in this case between web services and security). Throughout this series client certificate authentication for SOAP web services has been explained in detail (two types, at TLS level and using WS-Security). I hope that the entries are useful, or at least interesting, for you. The last maven project for the application can be download from here.
Certificate regards!
Comments