I spent a good few hours lately trying to understand how authentication and authorization work inside ServiceMix 3. It is this gathered knowledge that I want to share in this post. For those in a hurry, there's an executive summary at the end. For this article I assume you know the basic concepts of JAAS. Some reference material on JAAS can be found here and here.
Such complex topic is easier to understand if it is based on a valid use-case that others can reproduce easily. A pretty good candidate is the cxf-ws-security demo in ServiceMix 3.2. Anyone not familiar with that demo might want to build and run it prior to reading on.
In addition I encourage you to debug through the ServiceMix source code while you read through this article. Simply set the SERVICEMIX_DEBUG=1 environment variable and attach a Java debugger to ServiceMix. You can then place breakpoints into the relevant source code that I discuss below and walk through the call stack and source code as you read along.
I will not try to discuss all of the security aspects in ServiceMix 3 here as such scope is far too broad. Rather this article is based on running the cxf-ws-security demo and aims to explain how an incoming SOAP/HTTP request gets authenticated and authorized in ServiceMix 3.
Alright, the stage is set, let's start.
The demo's use-case is
External CXF Java client -> CXF-BC consumer -> CXF-SE component
as shown in the demo's README.txt. With regards to security we want to make sure that the client gets authenticated based on a username/password combination before verifying if the client has permissions to call the service (authorization). Sounds simple enough but it involves a good number of components as well we see next.
As documented in the cxf-ws-security demo, an external SOAP client sends a SOAP/HTTP request to the CXF-BC consumer component deployed in ServiceMix. The SOAP request includes a large WSSE Security header carrying a WS-Security UsernameToken profile among an XML-Signature and some XML-Encrypted content. I will not focus on XML-Signature and XML-Encryption here (some information on this regards was posted previously). For this discussion the UsernameToken profile is of most interest and is included in the SOAP request as follows (omitting some details for clarity):
<soap:Envelope xmlns:soap="..." >
<soap:Header>
...
<wsse:Security>
<wsse:UsernameToken>
<wsse:Username>alice</wsse:Username>
<wsse:Password>password</wsse:Password>
</wsse:UsernameToken>
...
It is this username/password combination in the SOAP security header that will be used for authentication inside ServiceMix.
So the request first hits the jetty server in the servicemix-cxf-bc component from where it gets passed into the CXF interceptor stack. There are a good couple of CXF interceptors configured by default. In our demo the list of CXF interceptors to be executed is as follows:
DEBUG - PhaseInterceptorChain:
receive [AttachmentInInterceptor, LoggingInInterceptor]
post-stream [StaxInInterceptor]
read [ReadHeadersInterceptor]
pre-protocol [MustUnderstandInterceptor, SAAJInInterceptor, WSS4JInInterceptor,
JbiJAASInterceptor]
unmarshal [JbiOperationInterceptor]
pre-invoke [JbiInWsdl1Interceptor, JbiInInterceptor]
invoke [JbiInvokerInterceptor, UltimateReceiverMustUnderstandInterceptor]
post-invoke [JbiPostInvokerInterceptor, OutgoingChainInterceptor]
Those interceptors in italics are of primary interest for this discussion.
In the configuration of the demo's ws-security-cxfbc-su component (in ws-security-cxfbc-su /src/main/resources/xbean.xml) we explicitly added and configured the org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor that will perform the WS-Security functions. It is the first security relevant interceptor that we want to look at closer.
org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor.handleMessage()
This interceptor handles the entire SOAP WS-Security header. In our example it needs to decrypt all the encrypted parts of the SOAP request, perform the XML-Signature check and extract the username/password information from the WS-Security header. What functions to perform is specified in xbean.xml, where this interceptor is configured (see the "action" list):
<bean class="org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor"
id="WSS4J">
<constructor-arg>
<map>
<entry key="action" value="Timestamp Signature Encrypt UsernameToken"/>
<entry key="..." .../>
...
</map>
</constructor-arg>
</bean>
The name tells it already, this interceptor uses Apache WSS4J to perform these WS-Security functions. This line of code inside CXF WSS4JInInterceptor.handleMessage() method calls into WSS4J:
Vector wsResult = getSecurityEngine().processSecurityHeader(
doc.getSOAPPart(),
actor,
cbHandler,
reqData.getSigCrypto(),
reqData.getDecCrypto()
);
I will not cover the Apache WSS4J specifics here but want to mention that the WSS4JInInterceptor configuration includes a password callback implementation that is used to obtain password for decryption and UsernameToken verification. This is also specified in the xbean.xml of the servicemix-cxf-bc-su component:
<entry key="passwordCallbackClass"
value="org.apache.servicemix.samples.cxf_ws_security.KeystorePasswordCallback"/>
See the Apache WSS4J documentation for more detailed information on WSS4J.
The outcome of each of the WS-Security function performed by WSS4J is stored in a java.util.Vector of org.apache.ws.security.WSSecurityEngineResult objects called wsResult that is then added as a property to the SoapMessage using the key WSHandlerConstants.RECV_RESULTS. This vector carries all the results from processing the WS-Security header, such as the principal, SAML token, X.509 certificates, etc.
Other interceptors that get invoked later will require these results for their own processing.
One of the vector elements will contain the result of parsing the WS-Security UsernameToken profile in form of a org.apache.ws.security.WSUsernameTokenPrincipal object instance. This hosts the username and password information that was received with the SOAP request.
That is roughly what the WSS4JInInterceptor does with our SOAP request.
The next CXF interceptor to be invoked is:
org.apache.servicemix.cxfbc.interceptors.JbiJAASInterceptor.handleMessage()
This one is invoked right after the WSSJ4InInterceptor and is rather complex. I will try my best to explain it in most simple terms.
It first of all extracts the WSHandlerConstants.RECV_RESULTS vector that was set by the previous WSS4JInInterceptor
// in JbiJAASInterceptor.handleMessage()
List<Object> results = (Vector<Object>)message.get(WSHandlerConstants.RECV_RESULTS);
and next checks if this Vector contains any org.apache.ws.security.WSUsernameTokenPrincipal object:
for (Iterator it = hr.getResults().iterator(); it.hasNext();) {
WSSecurityEngineResult er = (WSSecurityEngineResult) it.next();
if (er != null && er.getPrincipal() instanceof
WSUsernameTokenPrincipal){
WSUsernameTokenPrincipal p =
(WSUsernameTokenPrincipal)er.getPrincipal();
subject.getPrincipals().add(p);
this.authenticationService.authenticate(subject, domain, p.getName(), p.getPassword());
}
}
If yes, then the principal gets added to the javax.security.auth.Subject instance and the authentication service gets invoked. It is this security Subject instance that will carry all credentials information after a successful authentication!
org.apache.servicemix.jbi.security.auth.impl.JAASAuthenticationService.authenticate()
JAAS will be used now to perform the authentication.
At first a new JAAS javax.security.auth.login.LoginContext gets created while also registering a JAAS callback handler:
//in method authenticate()
LoginContext loginContext = new LoginContext(domain, subject, new CallbackHandler() {
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
for (int i = 0; i < callbacks.length; i++) {
if (callbacks[i] instanceof NameCallback) {
((NameCallback) callbacks[i]).setName(user);
} else if (callbacks[i] instanceof PasswordCallback && credentials instanceof String) {
((PasswordCallback) callbacks[i]).setPassword(((String) credentials).toCharArray());
} else if (callbacks[i] instanceof CertificateCallback && credentials instanceof X509Certificate) {
((CertificateCallback) callbacks[i]).setCertificate((X509Certificate) credentials);
} else {
throw new UnsupportedCallbackException(callbacks[i]);
}
}
}
});
This callback handler that we pass into the LoginContext constructor is able to deal with different types of callbacks (see the handle() method) and will be invoked later in order to take the username and password of the incoming request and place it into the callback object.
Once the LoginContext got instantiated, we can perform the authentication by calling
loginContext.login();
This API call internally invokes the configured JAAS login modules to perform their respective types of authentication (username/password based or certificate based, etc). If the call to login() returns without throwing an exception, the overall authentication succeeded. It is therefore the login module implementation that really decides whether a client is authenticated or not.
How are the login modules configured resolved and configured at runtime? Well, the domain argument of the LoginContext constructor specifies the index into a configuration that determines which LoginModules to use for the authentication. In our case the domain value will be "servicemix-domain", which matches the domain defined in conf/login.properties:
/* conf/login.properties */
servicemix-domain {
org.apache.servicemix.jbi.security.login.PropertiesLoginModule
sufficient
org.apache.servicemix.security.properties.user="users-passwords.properties"
org.apache.servicemix.security.properties.group="groups.properties";
org.apache.servicemix.jbi.security.login.CertificatesLoginModule
sufficient
org.apache.servicemix.security.certificates.user="users-credentials.properties"
org.apache.servicemix.security.certificates.group="groups.properties";
};
By default this registers two login modules which will be invoked in order until the call to login() succeeds. Actually the JAAS javax.security.auth.login.LoginModule will invoke initialize(), login() and commit() on the configured login modules in this order. Each login module class gets configured for a bunch of config files (we'll explore that later).
So the call to loginContext.login() internally invokes
- PropertiesLoginModule.initialize()
- PropertiesLoginModule.login()
- PropertiesLoginModule.commit()
in this order. If the call to any of these methods raises an exception, the CertificatesLoginModule will be tried. If the CertificatesLoginModule also raises an exception, then the authentication fails.
The various classes used in JAASAuthenticationService.authenticate() method (LoginContext, LoginModule, CallbackHandler, etc) are pure JAAS terminology and not ServiceMix specific. If you are not familiar with these, check the JAAS documentation.
Time to examine the org.apache.servicemix.jbi.security.login.PropertiesLoginModule next and see how it performs the authentication.
org.apache.servicemix.jbi.security.login.PropertiesLoginModule
This JAAS login module is used for username/password based login and is configured for two property files in conf/login.properties. The file users-passwords.properties (to be found in your ServiceMix conf/ directory) stores usernames and their passwords. The other properties file groups.properties maps users to their roles. If authentication succeeds, each authenticated user gets one or more roles assigned, according to this properties file. The authorization decision that is performed later will be done on the user's role, not the user's name (role based access control)!
PropertiesLoginModule.initialize() does not do too much, it only resolves the two property file names from the login module configuration in login.properties.
In PropertiesLoginModule.login() these files get loaded into memory. Notice that any changes to either users-passwords.properties or groups.properties will be reloaded when dealing with the next invocation! So you can adjust the user and role mapping at runtime.
The next step is to instantiate a javax.security.auth.callback.Callback array containing a NameCallback and a PasswordCallback.
// in method PropertiesLoginModule.login()
Callback[] callbacks = new Callback[2];
callbacks[0] = new NameCallback("Username: ");
callbacks[1] = new PasswordCallback("Password: ", false);
These callbacks are then passed into the callback handler that was created with the LoginContext earlier in JAASAuthenticationService.authenticate() (see previous sample code above):
//in method PropertiesLoginModule.login()
callbackHandler.handle(callbacks);
The callback handler now iterates through all the callbacks (in our case two, namely the NameCallback and PasswordCallback) and checks of what type they are. Check the code that I quoted above already:
//in method JAASAuthenticationService.authenticate()
LoginContext loginContext = new LoginContext(domain, subject, new CallbackHandler() {
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
for (int i = 0; i < callbacks.length; i++) {
if (callbacks[i] instanceof NameCallback) {
((NameCallback) callbacks[i]).setName(user);
} else if (callbacks[i] instanceof PasswordCallback && credentials instanceof String) {
((PasswordCallback) callbacks[i]).setPassword(((String) credentials).toCharArray());
} else if (callbacks[i] instanceof CertificateCallback && credentials instanceof X509Certificate) {
((CertificateCallback) callbacks[i]).setCertificate((X509Certificate) credentials);
} else {
throw new UnsupportedCallbackException(callbacks[i]);
}
}
}
});
For the NameCallback it sets the username that was extracted from the WS UsernameToken profile of the incoming SOAP request. And for the PasswordCallback it sets the corresponding password. If the callback was of type CertificateCallback, it would set the X.509 certificate of the incoming WSSE security header (this is not the case in our example).
Next we jump back into our PropertiesLoginModule.login() method. The two callback objects now contain the client's username and password (as sent with the SOAP request), so we can authenticate them.
The rest is very simple. The username and password extracted from the callback are compared to the user/password combinations loaded from users-passwords.properties and if they don't math an FailedLoginException is raised.
// method PropertiesLoginModule.login() continued
user = ((NameCallback) callbacks[0]).getName();
char[] tmpPassword = ((PasswordCallback) callbacks[1]).getPassword();
if (tmpPassword == null) {
tmpPassword = new char[0];
}
String password = users.getProperty(user);
if (password == null) {
throw new FailedLoginException("User does not exist");
}
if (!password.equals(new String(tmpPassword))) {
throw new FailedLoginException("Password does not match");
}
It is this code where the authentication either succeeds or fails. If authentication fails, a FailedLoginException is raised, causing the call to PropertiesLoginModule.abort(), otherwise PropertiesLoginModule.commit() is invoked. Inside commit() we only add the relevant security roles from groups.properties to the authenticated user and add the entire authenticated principal to the java security Subject:
public boolean commit() throws LoginException {
principals.add(new UserPrincipal(user));
for (Enumeration enumeration = groups.keys(); enumeration.hasMoreElements();) {
String name = (String) enumeration.nextElement();
String[] userList = ((String) groups.getProperty(name) + "").split(",");
for (int i = 0; i < userList.length; i++) {
if (user.equals(userList[i])) {
principals.add(new GroupPrincipal(name));
break;
}
}
}
subject.getPrincipals().addAll(principals);
...
}
The Java security Subject now carries all credential information including its user roles.
The list of roles will be required at last to perform authorization.
Note: All these operations are performed from within JbiJAASInterceptor.handleMessage(). This interceptor handles all the complex JAAS authentication part. Thus when this interceptor finishes, the client request is either authenticated or rejected. The remaining CXF interceptors can now be invoked but they won't perform any security related functions.
Finally the
org.apache.servicemix.cxfbc.JbiInvokerInterceptor.handleMessage()
will use the org.apache.servicemix.jbi.security.SecuredBroker to place the constructed JBI message onto the ServiceMix Normalized Message Router (NMR). But before doing so, the SecuredBroker will check if the caller is authorized. So this is the final authorization.
org.apache.servicemix.jbi.security.SecuredBroker.sendExchangePacket()
Before the message is sent to the NMR, the SecuredBroker checks if the caller is authorized. Once the JBI message containing the payload of the SOAP message is put onto the NMR it will be delivered to the target service (in our case the demo's ws-security-cxfse-su component) without performing any security checks. So this authorization is the final security operation in our scenario.
The information about what roles are allowed to invoke on what services are specified in conf/security.xml in your ServiceMix installation. Out of the box there is an <authorizationMap> entry as follows:
<sm:authorizationMap id="authorizationMap">
<sm:authorizationEntries>
<sm:authorizationEntry service="*:*" roles="*" />
</sm:authorizationEntries>
</sm:authorizationMap>
So out of the box anyone can invoke on any service. This basically turns off authorization. The source code actually checks if roles="*" and bypasses authorization completely:
// inside SecuredBroker.sendExchangePacket()
if (!acls.contains(GroupPrincipal.ANY)) {
//perform authorization, details omitted.
}
... whereby GroupPrincipal.ANY resolves to "*".
If you want to enable authorization you need to specify one or more authorization entries in security.xml where the role are not just "*", meaning everyone. You would generally list some of the role names from your conf/groups.properties file.
Of course you can fine grain access control to a particular service by specifying the service attribute in the <authorizationEntry> element. The syntax is
<sm:authorizationEntry service="{namespace}:servicename" roles="admin"/>
as typically defined in your xbean.xml service configuration. So in our example, in order to limit access to only the administrator role for the cxf-se service, you would specify:
<sm:authorizationEntry service="{http://apache.org/hello_world_soap_http}:SOAPServiceWSSecurity"
roles="admin" />
So this list of authorization entry configuration from security.xml is available to the SecuredBroker. On the other hand it also has access to the javax.security.auth.Subject instance that contains all the clients security credentials including the authenticated user roles and it can extract the service name of the target service to invoke on from the JBI MessageExchange. Hence it is just a question of checking that the user role in the security Subject is allowed to invoke the service named in the MessageExchange. This check will be performed using the authorization configuration from security.xml.
If the caller is not authorized a security exception will be thrown, otherwise the exchanges gets put onto the NMR from where it will be routed to the right service.
That's it. In case the authorization succeeds the JBI message is put onto the NMR from where it will be routed and dispatched to the target service.
Executive Summary:
- The WSS4JInInterceptor runs the WS-Security related functions, extract WS username and password from the SOAP security header and puts it into a WSUsernameTokenPrincipal object.
- The JbiJAASInterceptor is the next CXF interceptor to invoke on and checks for the presence of a WSUsernameTokenPrincipal, extracts the credential information and invokes on the JAAS authentication service.
- The JAAS authentication service uses the configured login module class (PropertiesLoginModule as set in conf/login.properties) to delegate the authentication decision.
- The PropertiesLoginModules calls back on the registered JAAS callback handler to retrieve the username/password and authenticates these credentials against the definitions in user-passwords.properties. If authentication succeeds, it assigns the list of roles to this authenticated Subject. The call stack returns all the way back to the JbiJAASInterceptor.
- The JbiInvokerInterceptor dispatches the request and uses the SecuredBroker to send the message exchange to the NMR.
- The SecuredBroker performs authorization based on the user role names stored in the Java security Subject and based on the authorization configuration made in conf/security.xml.
I hope I could spot some light onto the mysteries of authentication/authorization in ServiceMix 3.
The ServiceMix documentation does unfortunately not document these concepts in much detail.
Looking forward to any feedback from you.
4 comments:
Torsten,
very helpful post. My challenge however is that the authorization is coarse-grained at a service level and I need fine-grained authorization at a operation-level. Thus, in my SE SU Impl code, how do I pull the javax.security.auth.Subject object off the NMR to inspect the individual CRUD-based roles to apply them to individual operators?
Kindly,
Brian
Thanks for your feedback Brian.
Indeed, ServiceMix only offers per endpoint based authorization and not per operation.
Typically you apply authorization before you hit the service engine.
But if you want to access the security subject and roles from inside your service engine, have a look at how this is done in the
org.apache.servicemix.jbi.security.SecuredBroker.sendExchangePacket() method.
You will need access to the JBI exchange object, from where you can extract the user roles:
exchange.getRole()
and the security subject:
exchange.getMessage("in").getSecuritySubject();
I hope this will help you.
Hi Torsten,
Thanks for your article, it helped confirm what I am doing.
I am wondering if you tried this with version 3.3.1?
In 3.3.1 I'm having an issue with how the service name is written in the authorizationEntry. I simply can't get it to work with a namespace that is a URN or URL (like you did). I have to use a namespace like 'xxx' (no colon and no braces).
For example, this works sm:authorizationEntry service="xxx:myService" roles="superuser"
But, neither
sm:authorizationEntry service="{urn:/myNS}:myService" roles="superuser"
nor
sm:authorizationEntry service="urn:/myNS:myService" roles="superuser"
work.
Do you know anything about this?
Regards,
Trudi.
Torsten,
It's a very good post that expalins the internal details of authentication/authorization.
Are there any improvements in servicemix for opereation-level authorization using LDAP?
As you said do we need to use exchange.getMessage("in").getSecuritySubject().getPrincipals() to get all the LDAP roles for the user which is in the SecuritySubject and do mannual check against the roles?
Thanks and Rgards,
Sanjay
Post a Comment