Principals from JAAS through CAS to Spring Security

This article is about a custom integration of:

Let's imagine a situation we have a developed username-password based JAAS Login Module implementing an authentication process.
The JAAS Login Module is used by CAS server as a authentication handler.
The CAS server is used by Spring Security via its CAS authentication provider plugin.

So far it is easy to implement by a lot of existing manuals and HOWTOs using just provided libraries with no need to extend or modify a code.

But what if the JAAS module contains also an authorization, implemented for instance by putting principals (roles) to a subject. To transmit the roles from JAAS module through CAS server to the Spring context we need to do some additional work as following.


Principal is a term used by JAAS and CAS in a different meaning. Spring Security module uses rather a term role. We will use the term a principal or a role in the meaning of user's group.

Collaboration

The collaboration among nodes is shown in the picture:


JAAS Login Module

There is no big deal with JAAS, we need just to implement the javax.security.auth.spi.LoginModule interface and put our logic to authenticate a user.

For authorization we will use our custom class CommonGroupPrincipal implementing the java.security.Principal interface.

By some logic we assign roles to the user (as in the following very simple example) optimally in the commit method:

principals.add(new CommonGroupPrincipal("ROLE_USER"));
if ("admin".equals(user)) {
    principals.add(new CommonGroupPrincipal("ROLE_ADMIN"));
}
subject.getPrincipals().addAll(principals);

Now we can use the modul in the CAS server. All we need to do is run an application server (Tomcat for instance) containing the CAS WAR file with a Java option defining the JAAS configuration file location.
Modify the run script of the application server as following (for Tomcat, Windows):

set JAVA_OPTS=%JAVA_OPTS% -Djava.security.auth.login.config=%CATALINA_HOME%/conf/jaas.config

and create the configuration file:

CAS {
    ttulka.test.auth.jaas.module.CasLoginModule required;
};

Alternatively the Java default java.security.auth.login.config could be used, or the Java option could be set directly from the code of CustomJaasAuthenticationManager (see below):

System.setProperty("java.security.auth.login.config", "/path/to/jaas/config/file");

Spring Security CAS plugin

We can setup the Spring Security CAS plugin in a usual way, but we will implement our custom authenticationUserDetailsService property from the CAS authentication provider bean:

<bean id="casAuthenticationProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
	<property name="authenticationUserDetailsService">
		<bean class="ttulka.test.auth.spring.CustomAuthenticationUserDetailsService" />
	</property>
	...
The class CustomAuthenticationUserDetailsService implements the org.springframework.security.core.userdetails.AuthenticationUserDetailsService interface and its only one method deals with an object representing a CAS response token: 
public class CustomAuthenticationUserDetailsService implements AuthenticationUserDetailsService<CasAssertionAuthenticationToken> {
    @Override
    public UserDetails loadUserDetails(CasAssertionAuthenticationToken token) throws UsernameNotFoundException {

        final String groupList = (String)token.getAssertion().getPrincipal().getAttributes().get("CommonGroupPrincipal");
        ...

The variable groupList is holding a roles list transmitted from the CAS server got from the JAAS module. We will use to build a list of the granted authorities returned as a part of the UserDetails object.

CAS server extension

To push a CAS to consume and transmit additional attributes as roles we need to implement our custom alternative of an authentication handler and its standard implementation by the class JaasAuthenticationHandler class. Unfortunately the desired method authenticateUsernamePasswordInternal is in the JaasAuthenticationHandler class defined as final, so we cannot just simply extend the class.

To work with our custom handler we need to extend the CAS authentication manager, too. As in the previous class, the standard implementation AuthenticationManagerImpl is set as a final class, so we have to create a new one.

JAAS Authentication Handler

Alike the JaasAuthenticationHandler we will extend the class org.jasig.cas.authentication.handler.support.AbstractUsernamePasswordAuthenticationHandler and implement just the method authenticateUsernamePasswordInternal.

Using the standard JAAS process we log in via credentials and get principals from the JAAS subject represented by CommonGroupPrincipal objects (see above).
From the list we create a comma-separated string and put it into a principals map by a key as the class name (a public accessible property of the handler class):

public class CustomJaasAuthenticationHandler extends AbstractUsernamePasswordAuthenticationHandler {
    ...
    private Map<String, Object> principals = new HashMap<>();
    ...
    protected boolean authenticateUsernamePasswordInternal(final UsernamePasswordCredentials credentials)...
        ...
        loginContext = new LoginContext(realm, handler);
        loginContext.login();
	   
        processPrincipals(loginContext.getSubject().getPrincipals());
        ...
    ...
    private void processPrincipals(Set<Principal> principalsSet) {
        ...
        for (Principal p : principalsSet) {
            if (p instanceof CommonGroupPrincipal) {
                sb.append(p.getName());
                ...
            }
        }
        principals.put(CommonGroupPrincipal.class.getSimpleName(), sb.toString());
        ...

The handler will be used directly by our new manager (see below) so needs no additional setting.
Alternatively we can define it in the CAS webapp configuration file (WEB-INF/deployerConfigContext.xml) as a new bean and then inject into the manager.

JAAS Authentication Manager

Alike the AuthenticationManagerImpl we will extend the class org.jasig.cas.authentication.AbstractAuthenticationManager and implement just the method authenticateAndObtainPrincipal.

After the authentication succeeds we build the principal's attributes from the handler's principals and return:

handler = new CustomJaasAuthenticationHandler();
...
Principal principal = new SimplePrincipal(((UsernamePasswordCredentials)credentials).getUsername(), handler.getPrincipals());

return new Pair<AuthenticationHandler,Principal>(handler, principal);

In the CAS webapp configuration file (WEB-INF/deployerConfigContext.xml) we need to set the manager:

<bean id="authenticationManager" class="ttulka.test.auth.cas.CustomJaasAuthenticationManager" />

To include the attributes with roles in the response to the Spring Security plugin we need to allow the attribute name CommonGroupPrincipal for a registered service in WEB-INF/deployerConfigContext.xml:

<bean id="serviceRegistryDao" class="org.jasig.cas.services.InMemoryServiceRegistryDaoImpl">
    <property name="registeredServices">
        <list>
            <bean class="org.jasig.cas.services.RegexRegisteredService">
                ... 
                <property name="allowedAttributes">
                     <list>
                        <value>CommonGroupPrincipal</value>
                     </list>
                </property>
            </bean> 
            ...

The last step is to modify the response renderer JSP page WEB-INF/view/jsp/protocol/2.0/casServiceValidatorSuccess.jsp to contain the attributes:

<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
    <cas:authenticationSuccess>
        <cas:user>${fn:escapeXml(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.id)}</cas:user>    
        <cas:attributes>
        <c:forEach var="attr" items="${assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes}">
           <cas:${fn:escapeXml(attr.key)}>${fn:escapeXml(attr.value)}</cas:${fn:escapeXml(attr.key)}>
        </c:forEach>
        </cas:attributes> 
        ... 

The response will then looks like:

<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
        <cas:authenticationSuccess>
              <cas:user>admin</cas:user>
              <cas:attributes>
                    <cas:CommonGroupPrincipal>ROLE_USER,ROLE_ADMIN</cas:CommonGroupPrincipal>
              </cas:attributes>
        </cas:authenticationSuccess>
</cas:serviceResponse>

The responsed XML is then processed by the Spring's CustomAuthenticationUserDetailsService (see above).

All the CAS-related classes must be packed as a JAR library and copied to /WEB-INF/lib folder of the CAS WAR application.

And the integration is ready to use!

Appendix

I am working with Java 7, CAS 3.5.2, Spring 3.2.4. and Spring Security module 3.2.0.

Please see the discussed code in the attachment.