Once upon a time…
Sometimes a new project becomes a challenge. That’s the interesting part of software development; otherwise, this job would not be as special as it is!
And the reason could not only be the technology you have to use, or the specific requirements you have, the out-of-your-hands environment architecture, the integrations with third-party software, or the I-follow-this-standard-but-true-is-I-only-do-it-partially situations you can find sometimes (it happens more than I’d wish), but a combination of one or more of them with something else.
… there was a Liferay and Okta integration …
I am going to talk about Liferay, SAML and multiple virtual hosts. Recently I worked on the integration of Liferay with Okta. On that project Okta is the entry point of all the client’s systems, the platform where users are managed, and the mechanism used by every app to integrate into the system. In this situation, Liferay’s SAML plugin is a must; it is a plugin that works great to integrate your systems, since Liferay can be either the Service Provider or the Identity Provider. SAML and Liferay work just fine.
In this case Liferay was going to be the Service Provider (SP) and Okta was going to be the Identity Provider (IdP). I installed, configured and tested the plugin and everything worked great, but there was one small issue: the client doesn’t have just one site in Liferay, but a considerable amount of them.
This is not an issue per se, but some of the sites in Liferay are configured as virtual hosts. This means that every site has a unique url, so for an external user they are completely different, as independent one of each other as www.google.com and www.apple.com are, for example. Some of you probably have started to see what the issue was…
… that needed a SAML plugin update …
Liferay’s SAML plugin is prepared to link one Liferay deployment with one entity, using one metadata file. You create the metadata and get the entity url from the IdP, you define if you are using ssl, the assertion signature, the mappings, the keystore…. plenty of fun, and you can find how to do it in a lot of places.
But this link between the IdP and the SP is done in a one to one basis, so you can link your Liferay site www.myliferaysite.com (or whatever your url is) with your provider.
For those of you who know how the SAML plugin works, you know that this should not be a big deal. Basically, the configuration is done in portal-ext.properties, so if you create multiple Liferay instances (companies), you can have one custom portal-ext.properties for each one and you can configure the SAML plugin once per instance. That should work, right?
… for multiple sites in the same Liferay instance with virtual hosting …
In this case, every site was in the same Liferay instance. So that approach is not valid. The only choice was to customize the SAML plugin to enable multiple sites in the same Liferay deployment to use it.
So I changed the SAML plugin to allow multiple entities configured in the same. Let’s see how I did it.
… and this is how I solved it:
First: Detect which classes will be overwritten
First, you must detect which classes will be affected. In our case, any file with a call to
MetadataManagerUtil.getDefaultIdpEntityId()
will be affected, as we do not have a default entity id but one entity id per site. So we had to change that method to check portal-ext.properties and get the entity id depending on the virtual url. So we implemented a method like this:
private String getIdpEntityIdFromVirtualHostLayoutSet(HttpServletRequest request)
throws Exception {
LayoutSet layoutSet = (LayoutSet)request.getAttribute("VIRTUAL_HOST_LAYOUT_SET");
boolean isPrivate = layoutSet.isPrivateLayout();
long groupId = layoutSet.getGroupId();
String key = String.valueOf(groupId);
if (isPrivate) {
key = key.concat(".1");
}
String idpEntityId = PropsUtil.get(PortletPropsKeys.SAML_SP_DEFAULT_IDP_ENTITY_ID,
new Filter(key));
return idpEntityId;
}
This method gets the virtual host layout set from the request; from there, we get the groupId, and if it is a private or a public layout (you can have different urls – and entities – for public and private sites of the same group). With the groupId we get a key, to which we add a “.1” at the end if it is private (just because I want it this way).
Second: Prepare your portal properties file
So I will have a key like “1641243” or “1641243.1”. With this key, I can go to my portal-ext.properties and get the entity id, from a property like
saml.sp.default.idp.entity.id[1641243]=entityIdUrl
Well, that’s the first point. The second point is to have the metadata loaded, but fortunately Liferay does this for you: the property
saml.metadata.paths=pathToYourMetadata
allows comma separated values, so just put the path to as many metadata files as you need in there. That should do it. And don’t forget to deploy the metadata files to your environment!
Of course, you also need to configure everything else that the SAML plugin needs: the keystore (you can use the same one for every entity), if you require ssl, the assertion signature, etc. To keep everything simple, we defined that every entity will be defined in the same way, so we will not have multiple definitions on this, but it should be possible to do it in the same way. Mappings also will be shared, as the origin system for entities is the same one!
Third: Do not overwrite original classes!!
Do not dare to overwrite any Liferay class!! To avoid that in the SAML plugin, you can just go to /WEB-INF/src/META-INF/saml-spring.xml, and link a new implementation for the classes you will create. Those classes could be copies of the original ones, where you add your custom code, or could be custom classes that extend the original ones. Your choice.
For example, instead of
<bean id="com.liferay.saml.profile.WebSsoProfile"
class="com.liferay.saml.profile.WebSsoProfileImpl">
we used
<bean id="com.liferay.saml.profile.WebSsoProfile"
class="com.xtivia.okta.saml.profile.WebSsoProfileImpl">
Four: Change anything else you need
In our case, the Okta logout required a redirect functionality that we changed in the SingleLogoutProfileImpl class (see the third step…) and a user import update that we changed in the UserResolver class (do not forget the third step, you know it is important). The logout redirect was improved with properties and a change in the portlet. This is so after logout you can be redirected to Okta, to the current site’s main page or to anywhere you want, depending on your requirements. But that’s another story…
Five: Create the apps (entities in Okta)
You should probably have created the apps before. It could even be the first step. Anyway, check this guide to view how to do it for one app. You will get the entityId and the metadata file in there, and the general configuration and the attribute mapping.
For multiple apps (in our case they were 23), you can clone an existing implementation. The process is simple and fast. If you are willing to create thousands of Liferay sites as Okta apps, you may want to dig into the Okta API. The API is pretty easy to use, and it has a feature to add a SAML 2.0 App. Even though I have used the Okta API, I have not used this add app feature. But with some research and a Liferay plugin, you should be able to remotely create as many Okta apps as you want!
Six: Deploy. And Test. You know how to do it.
blah, blah, blah. Pay special attention to an issue found in custom themes: the SAML integration stops working if your portal is https and the sign in link in the theme is like this:
<a href="$sign_in_url" id="sign-in" rel="nofollow">$sign_in_text</a>
instead of this:
<a data-redirect="$is_login_redirect_required" href="$sign_in_url"
id="sign-in" rel="nofollow">$sign_in_text</a>