Integrating german Servicekonto with Liferay

A customer wanted to integrate the german Servicekonto with Liferay. The Servicekonto is an authentication service used by (or at least planned to be used) by several german federal states. It offers an authentication mechanism for citizens and organizations. Every citizen can create an account using the new german identity card. As the Servicekonto offers to authenticate via OpenID connect, it should have been easy to integrate with Liferay.

So the customer configured OpenID connect in Liferay and tried to login with a Servicekonto account. Everything was properly configured, but the login failed with a strange error message:

2019-06-28 09:32:11.331 ERROR [https-jsse-nio-8443-exec-1][OpenIdConnectFilter:132] Unable to process the OpenID login
java.lang.NullPointerException at com.liferay.portal.security.sso.openid.connect.internal.OpenIdConnectUserInfoProcessorImpl.processUserInfo(OpenIdConnectUserInfoProcessorImpl.java:49)

We were not sure at first why it failed. An OpenID connect configuration for e.g. Google worked perfectly. After some research we found that it had to do something with the UserInfo Endpoint. That Endpoint is used to fetch additional information about the user after a successful login. Liferay e.g. uses the e-ail address to match the user against an existing Liferay user and to create a new user if it does not exist, yet. As section 5.3.2 of the OpenID connect specification says it is possible that the UserInfo Endpoint returns either JSON format or a JWT encryted message. The Servicekonto seems to return a JWT encrypted message. The Liferay implementation does not handle this case, the call to userInfoSuccessResponse.getUserInfo() returns null. The Liferay implementation does not check userInfoSuccessResponse.getUserInfoJWT()

Creating a test environment

To test if the above situation causes the problems we created a test environment. As there is no "local" installation of the Servicekonto we decided to setup a local Keycloak instance. Fortunately Keycloak offers a prepared Docker image, so installation was rather easy. After creating a client in Keycloak we configured it to return a JWT encrypted message for the UserInfo Endpoint. This can be done at Configure > Clients > Settings > Fine Grain OpenID Connect Configuration > User Info Signed Response Algorithm.

With this configuration Liferay threw the same exception as above.

To extend or to overwrite?

OpenID connect is handled in Liferay in the OpenIdConnectServiceHandlerImpl class. So the first thought was to replace the whole module with our own implementation. However we do not want to loose updateabilty, so we decided to just create a new component for the OpenIdConnectServiceHandler with a higher service ranking, so that Liferay would load our implementation instead of the default one.

Some problems arose by this: The default implementation uses Nimbus to handle the OpenID connection stuff. If our own module would deliver its own jars for Nimbus the classes would be incompatible and would throw a ClassCastException (same classes, but loaded by different classloaders). So we wrote a fragment for the default implementation and just added the required packages as Export-Package. By this we were able to re-use the existing libraries from the default implementation (David Nebinger has an interesting blog post on how to do that).

After fixing that we discovered that even if our implementation had a higher service ranking than the default implementation our component was not used. The reason behind this is that the references where the components are injected are reluctant. That means if a reference is already injected, it will not be replaced by a higher ranked component automatically. However it will be replaced if the service is reconfigured so that the previously injected component does no longer match the criterias. This can be done by creating new configuration files for the relevant components. The Liferay documentation shows how do that.

Our own implementation now handles JWT encrypted responses from the UserInfo Endpoint and falls back to the default implementation if the endpoint answers with a plain JSON response.

The Result

Logins using OpenID connect with a JWT encrypted answer from the UserInfo Endpoint now work properly. By this the customer can successfully login using the Servicekonto (and other systems with a JWT encrypted answer). The following screens show a working login using our test environment with Keycloak.

Final Notes

In a strict sense the "missing" functionality should be considered as a bug in Liferay. The above was done with Liferay 7.1.3 GA 4. As far as I can see the default implementation was not changed in 7.2 and in the master branch. 

 Liferay creates a user when logging in using OpenID connect. To make it work, you have to enable the Option allow strangers to create accounts.

 

Blogs