Liferay 7.4-ga112, Keycloak & OpenID Connect : Login not working

Olaf Kock, modified 1 Year ago. New Member Posts: 5 Join Date: 10/19/21 Recent Posts

Hello Everyone, I am trying to Connect Liferay with Keycloak, But after login, I get the below screen.

 

And the error:

2024-06-04 11:36:37.734 ERROR [http-nio-9015-exec-5][StatusDisplayContext:83] Error: null
2024-06-04 11:37:52.841 WARN  [http-nio-9015-exec-8][PortalImpl:6128] null
java.lang.NullPointerException: null
    at com.liferay.portal.security.sso.openid.connect.internal.OIDCUserInfoProcessor._getUserId(OIDCUserInfoProcessor.java:514) ~[bundleFile:?]
    at com.liferay.portal.security.sso.openid.connect.internal.OIDCUserInfoProcessor.processUserInfo(OIDCUserInfoProcessor.java:60) ~[bundleFile:?]
    at com.liferay.portal.security.sso.openid.connect.internal.OpenIdConnectAuthenticationHandlerImpl.processAuthenticationResponse(OpenIdConnectAuthenticationHandlerImpl.java:143) ~[bundleFile:?]
    at com.liferay.portal.security.sso.openid.connect.internal.servlet.filter.auto.login.OpenIdConnectAutoLoginFilter.processFilter(OpenIdConnectAutoLoginFilter.java:87) [bundleFile:?]
    at com.liferay.portal.kernel.servlet.BaseFilter.doFilter(BaseFilter.java:40) [portal-kernel.jar:?]    

This is my OpenIdUserInfoConnectorClass:

package com.abc.xyz.portal.security.sso.oidc;

import com.abc.xyz.portal.security.sso.oidc.util.UserGroupUtil;
import com.abc.xyz.portal.security.sso.oidc.util.UserUtil;
import com.liferay.petra.string.StringBundler;
import com.liferay.petra.string.StringPool;
import com.liferay.portal.kernel.exception.PortalException;
import com.liferay.portal.kernel.exception.UserEmailAddressException;
import com.liferay.portal.kernel.json.JSONArray;
import com.liferay.portal.kernel.json.JSONFactoryUtil;
import com.liferay.portal.kernel.json.JSONObject;
import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil;
import com.liferay.portal.kernel.model.Company;
import com.liferay.portal.kernel.model.Group;
import com.liferay.portal.kernel.model.User;
import com.liferay.portal.kernel.service.CompanyLocalService;
import com.liferay.portal.kernel.service.ContactLocalService;
import com.liferay.portal.kernel.service.GroupLocalService;
import com.liferay.portal.kernel.service.UserGroupLocalService;
import com.liferay.portal.kernel.service.UserLocalService;
import com.liferay.portal.kernel.util.Validator;
import com.liferay.portal.security.sso.openid.connect.OpenIdConnectServiceException;
import com.liferay.portal.security.sso.openid.connect.OpenIdConnectServiceException.UserMappingException;
import com.liferay.portal.security.sso.openid.connect.internal.OIDCUserInfoProcessor;
import com.liferay.portal.security.sso.openid.connect.internal.exception.StrangersNotAllowedException;
import com.nimbusds.openid.connect.sdk.claims.UserInfo;

import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;


@Component(
        immediate = true,
        property = {"service.ranking:Integer=100"},
        service = OIDCUserInfoProcessor.class
)
public class PortalOpenIdConnectUserInfoProcessor extends OIDCUserInfoProcessor {

    private static final String USER_INFORMATION = "userInformation";
    private static final String SCREEN_NAME = "username";
    private static final String EMAIL_ADDRESS = "email";
    private static final String FIRST_NAME = "firstName";
    private static final String LAST_NAME = "lastName";
    private static final String PERSON_ID = "personId";
    private static final String ROLES = "roles";
    private static final String INDUSTRY = "industry";


    public long processUserInfo(UserInfo userInfo, long companyId)
            throws PortalException {

        _log.debug("userInfo: " + userInfo.toJSONObject().toJSONString());

        //Keycloak returns response in JSON format, so creating JSON object from response. Later extracting industry name from the JSON Object
        JSONObject userInfoJSON = JSONFactoryUtil.createJSONObject(userInfo.toJSONObject().toJSONString());
        JSONObject customUserInfo = userInfoJSON.getJSONObject(USER_INFORMATION);

        checkUserInfoValidity(customUserInfo);

        String screenName = customUserInfo.getString(SCREEN_NAME);
        String emailAddress = customUserInfo.getString(EMAIL_ADDRESS);
        String firstName = customUserInfo.getString(FIRST_NAME);
        String lastName = customUserInfo.getString(LAST_NAME);
        String personId = customUserInfo.getString(PERSON_ID);
        _log.info("ScreenName ooutput -1 =====================================================>>"+screenName);
        screenName = getUsernamefromEmailAddress(screenName);
        _log.info("ScreenName ooutput=====================================================>>"+screenName);
        checkUserProfileValidity(screenName, emailAddress, firstName, lastName, personId);


        //ADDED FOR MULTI-TENANCY. IDENTIFYING THE SITE(GROUP) FROM INDUSTRY NAME SO THAT THE USER CAN BE ADDED TO MEMBERSHIP OF THE SITE
        String industry = userInfoJSON.getString(INDUSTRY);
        Group site = null;
        long groupId = 0;
        try
        {
            site = _groupLocalService.getFriendlyURLGroup(companyId, StringPool.SLASH + industry); //find site(group) that has friendly url /industry_name
            groupId = site.getGroupId();
        }
        catch (PortalException e)
        {
            _log.error("Error occured while trying to find site from tenant:"+industry, e);
        }


        JSONArray roles = customUserInfo.getJSONArray(ROLES);
        String[] roleNames = getRoleNames(roles);
        long[] userGroupIds = UserGroupUtil.getUserGroupIds(companyId, roleNames, _userGroupLocalService);

        User user = null;
        String industryUserScreenName = screenName+StringPool.AT+industry;

        user = _userLocalService.fetchUserByScreenName(companyId, industryUserScreenName);
        if (user != null) {
            UserUtil.updateUser(user, companyId, industryUserScreenName, emailAddress, firstName, lastName, personId, userGroupIds, _companyLocalService, _userLocalService, _userGroupLocalService,  _contactLocalService, groupId);
            return user.getUserId();
        }

        checkAddUser(companyId, emailAddress);

        user = UserUtil.addUser(companyId, industryUserScreenName, emailAddress, firstName, lastName, personId, userGroupIds, _companyLocalService, _userLocalService, _contactLocalService, groupId);

        return user.getUserId();
    }


    private String[] getRoleNames(JSONArray roles) {
        String[] roleNames = new String[0];

        if (Validator.isNotNull(roles)) {
            roleNames = new String[roles.length()];
            for (int i = 0; i < roles.length(); i++) {
                roleNames[i] = roles.getString(i);
            }
        }

        return roleNames;
    }


    private void checkUserInfoValidity(JSONObject customUserInfo) throws UserMappingException  {
        if (customUserInfo == null) {
            throw new OpenIdConnectServiceException.UserMappingException("Invalid OIDC user info: userInformation claim is missing!");
        }

        String message = customUserInfo.getString("message");
        String status = customUserInfo.getString("status");
        String data = customUserInfo.getString("data");

        if (Validator.isNotNull(message)) {
            StringBundler sb = new StringBundler(9);

            sb.append("Unable to map OpenId Connect user to the portal, ");
            sb.append("Received error data: ");
            sb.append("{status=");
            sb.append(status);
            sb.append(", message=");
            sb.append(message);
            sb.append(", data=");
            sb.append(data);
            sb.append("}");

            throw new OpenIdConnectServiceException.UserMappingException(
                    sb.toString());
        }
    }

    private void checkUserProfileValidity(String screenName, String emailAddress, String firstName, String lastName,
                                          String personId) throws UserMappingException {

        if (Validator.isNull(screenName) || Validator.isNull(firstName) || Validator.isNull(lastName)
                || Validator.isNull(personId)) {

            StringBundler sb = new StringBundler(9);

            sb.append("Unable to map OpenId Connect user to the portal, ");
            sb.append("missing or invalid profile information: ");
            sb.append("{screenName=");
            sb.append(screenName);
            sb.append(", emailAddress=");
            sb.append(emailAddress);
            sb.append(", firstName=");
            sb.append(firstName);
            sb.append(", lastName=");
            sb.append(lastName);
            sb.append(", personId=");
            sb.append(personId);
            sb.append("}");

            throw new OpenIdConnectServiceException.UserMappingException(
                    sb.toString());
        }
    }

    public static String getUsernamefromEmailAddress(String userName){
        try{
            if(userName.contains("@")){
                String email = userName;
                int index = email.indexOf('@');
                email = email.substring(0,index);
                return email;
            }else{
                return userName;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return userName;

    }

    protected void checkAddUser(long companyId, String emailAddress)
            throws PortalException {

        Company company = _companyLocalService.getCompany(companyId);

        if (!company.isStrangers()) {
            throw new StrangersNotAllowedException(companyId);
        }

        if (!company.isStrangersWithMx() &&
                company.hasCompanyMx(emailAddress)) {

            throw new UserEmailAddressException.MustNotUseCompanyMx(
                    emailAddress);
        }
    }

    @Reference
    private CompanyLocalService _companyLocalService;

    @Reference
    private UserLocalService _userLocalService;

    @Reference
    private UserGroupLocalService _userGroupLocalService;

    @Reference
    private ContactLocalService _contactLocalService;

    @Reference
    private GroupLocalService _groupLocalService;

    private static Log _log = LogFactoryUtil.getLog(PortalOpenIdConnectUserInfoProcessor.class);

}

To execute this, I have added below property in com.liferay.portal.security.sso.openid.connect.internal.OpenIdConnectServiceHandlerImpl.config file under osgi/configs.

_oIDCUserInfoProcessor.target = "(component.name=com.abc.xyz.portal.security.sso.oidc.PortalOpenIdConnectUserInfoProcessor)"

 

I am trying to identify the issue, but couldn't find anything. Can someone help me on this.

 

Best Regards

Akash

 

thumbnail
Zsigmond Rab, modified 1 Year ago. Liferay Master Posts: 764 Join Date: 1/5/10 Recent Posts

Hi Akash,

Taking a quick look at the exception, it seems still the original method is called which throws an NPE from the https://github.com/liferay/liferay-portal/blob/7.4.3.120-ga120/modules/apps/portal-security-sso/portal-security-sso-openid-connect-impl/src/main/java/com/liferay/portal/security/sso/openid/connect/internal/OIDCUserInfoProcessor.java#L514 line.

This could be investigated to find the reason.

I's also suspicious that the parameters are different for your processUserInfo method.

Here you can find a public custom OIDC user resolver: https://github.com/fabian-bouche-liferay/oidc-user-resolver/tree/master.

Regards,
Zsigmond

Jamie Sammons, modified 1 Year ago. New Member Posts: 5 Join Date: 10/19/21 Recent Posts

Thank you for your answer Rab,

The exception is coming in the default Liferay class, not in the class I have overwritten. Our custom class is not getting executed (overriding).

In 7.2 this is how we were using to inforce our custom class to override the liferay's class.

com.liferay.portal.security.sso.openid.connect.internal.OpenIdConnectServiceHandlerImpl.config file under osgi/configs.

_oIDCUserInfoProcessor.target = "(component.name=com.abc.xyz.portal.security.sso.oidc.PortalOpenIdConnectUserInfoProcessor)"

 

But this is not the case in the 7.4.

We do not think, this should the the correct file name "com.liferay.portal.security.sso.openid.connect.internal.OpenIdConnectServiceHandlerImpl.config " under osgi/configs to resolve this issue.