Overriding CASFilter in Liferay 7.3 to support multiple domains

Liferay has great integration tools with 3rd party authentication systems and thanks to the recent changes in Control Panel, they are much easier to configure than before. Some clients, however, have more specific requirements, which go beyond possible configuration options and require implementing custom modifications directly on the Liferay integration code. A quite common requirement (I did it at least 3 times already), is support for multi-domain logging with CAS. The exact problem description is as follows:

  • There is a Portal integrated with Jasig CAS, using standard Instance settings which include CAS server details and our site URL (called serverName) which points to a default site on our Liferay instance.

  • The administrator adds a new Liferay Site and configures a separate domain for its Public Pages.

  • Logging in with CAS on the default site (mysite.local) works well, but
  • trying to log in with a subsite (mysubsite.local) fails, as CAS always redirects the user back to default site mysite.local.

The reason why this happens is the configuration scope, which is common for the entire instance and the parameter serverName cannot be set for a specific site.

Implementing CASFilter override

This is the moment when configuration powers end and we have to go deeper into the code and override the default CAS integration Filter implementation. This used to be quite difficult before Liferay 7.0 as it required using the EXT environment, which was a dirty hack around Liferay portal-impl context, but nowadays with OSGi infrastructure, it is much much easier.

tl;dr

I you're just interested in the solution, you can find it in my repository https://github.com/kgolebiowski/liferay-multidomain-cas-integration, which contains a full Liferay workspace (build against 7.3-ga3) with a module that provides an implementation of a simple solution for the problem. Basically it takes the first available Virtual Hostname for the site and uses it to generate CAS service URL and if it's empty uses standard Liferay CAS settings.

How was it done?

Let's assume we have a properly configured workspace for our current project which already has a liferay.workspace.product set (successor of liferay.workspace.target.platform.version).

1. Create empty module

That can be done by either blade create, by using the API template or just by creating bnd.bnd and build.gradle files that contain standard dependencies. The related commit is available here.

2. Copy original CASFilter file from Liferay sources

We'd like to modify a Liferay source file, so the first step is to find it in Liferay sources and copy to our project. In terms of the naming convention, I usually keep the same package path as in Liferay sources and I add my personal/company prefix at the beginning.  Then new name here would be ski.golebiow.liferay.security.sso.cas.internal.servlet.filter.MultidomainCASFilter.  The source file CASFilter is located under portal-security-sso-cas-impl module and the related commit is here.

3. Add CASFilter compile dependencies

The class is not compiling yet so we need a few dependencies to be able to properly build it.

  • Liferay SSO CAS API package added in build.gradle

compile group: "com.liferay", name: "com.liferay.portal.security.sso.cas.api"

  • The actual Jasig CAS Client added in build.gradle

compile group: "org.jasig.cas.client", name: "cas-client-core", version: "3.6.1"

4. Adjust OSGi runtime configuration

At this point, the project builds without any errors. Problems start if we try to deploy it in Liferay as even though the original Jasig CAS client is shipped together with the original portal (it is embedded in the main bundle), it's not exported with an OSGi. It's also not an OSGi bundle, so the only thing we can do here is to repackage and include it in our final JAR. In order to do it, the following needs to be added to bnd.bnd file:

Include-Resource: \
 @cas-client-core-3.6.1.jar

Some may say here that I should use the new compileInclude directive from Gradle which is better as it also includes all the transitive dependencies, but unfortunately, it won't work in this case. It seems there is a conflict between org.bouncycastle:bcpkix-jdk15on:1.63 and org.bouncycastle:bcprov-jdk15on:1.63 which results in build failure when we try to put it all together in a single JAR file. Finally, we have to manage transitive dependencies on our own. It is not difficult though and requires two actions:

Import-Package: \
 org.apache.commons.codec.binary; version="[1.9,2)",\
 *

  • Upload the two transitive dependencies bcpkix-jdk15on-1.63.jar and bcprov-jdk15on-1.63.jar to LIFERAY_HOME/osgi/modules/ directory. Liferay's main bundle already contains those two, but for some reason, they are not exported. Fortunately, they are regular OSGi bundles so deploying them within OSGi container makes them resolvable by our module.

5. Update service ranking

The last thing we need to do before implementing the actual change is increasing service ranking on our MultidomainCASFilter, so it has a higher priority than the default one.

@Component(
        configurationPid = "com.liferay.portal.security.sso.cas.configuration.CASConfiguration",
        immediate = true,
        property = {
                "before-filter=Auto Login Filter", "dispatcher=FORWARD",
                "dispatcher=REQUEST", "servlet-context-name=",
                "servlet-filter-name=SSO CAS Filter", "url-pattern=/c/portal/login",
                "url-pattern=/c/portal/logout",
                "service.ranking:Integer=100"
        },
        service = Filter.class
)

 6. Implement the change!

At this point, our MultidomainCASFilter gets deployed, properly activated within Liferay's OSGi context, and used instead of the original CASFilter. The only missing part is the actual implementation of our requirement. 

The point of this blog was to focus mainly on CASFilter override and all the OSGi related things, so the implementation is the simplest possible and does not cover all the cases (e.g. different domains for private and public pages). Basically it takes the first available VirtualHostname and uses it to generate CAS service URL and if none has been found, takes the default settings.

LayoutSet layoutSet =
        (LayoutSet)httpServletRequest.getAttribute(WebKeys.VIRTUAL_HOST_LAYOUT_SET);

String serverName =
        layoutSet.getVirtualHostnames().entrySet().stream().findFirst()
                .map(entry -> "https://" + entry.getKey() + "/")
                .orElse(casConfiguration.serverName());

The commit to introducing the change to the original CASFilter is available here.

Summary

Voilà! Just put the generated jar file in your deploy or modules folder and it will work straight away.

I know that with the introduction of OSGi and the high level of modularization of Liferay Portal core, there is no more "the one single solution" for a problem. Now we can do things in many ways, so I suppose some of you might have implemented the multidomain CAS login in other, possibly better way. Let me know in the comments :)

Blogs