Liferay Keycloak integration - SSO and SLO implementation

Liferay Keycloak - Single Sign On / Single Logout


Overview
Previous article ( https://lifedev-solutions.blogspot.com/2019/10/liferay-keycloak-integration-using.html ) describes steps to configure Keycloak SSO Authorization in Liferay using OpenID Connect Provider.
However, this approach has several limitations:
- it can't be used as a default login mechanism: user needs to click "Sign In", chose "OpenId Connect", select "OpenId Connect Provider", and only then he is redirected to the Keycloak sign-in form;
- After logging out from Liferay user stays logged in in Keycloak.
This article will show how to overcome these issues.

SSO with Servlet Filter
For automatic redirection to Keycloak's Sign In form the servlet filter may be implemented:
@Component(
        immediate = true,
        property = {
                "servlet-context-name=",
                "servlet-filter-name=Keycloak Login Filter",
                "url-pattern=/c/portal/login"        },
        service = Filter.class)
public class KeycloakLoginFilter implements Filter {

    @Override    public void init(FilterConfig filterConfig) {
    }

    @Override    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, 
                         FilterChain filterChain) throws IOException, ServletException {
        try {
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            HttpServletResponse response = (HttpServletResponse) servletResponse;

            //Get OpenId Providers            Collection<String> openIdConnectProviderNames = 
                    _openIdConnectProviderRegistry.getOpenIdConnectProviderNames();
            if (openIdConnectProviderNames == null || openIdConnectProviderNames.size() == 0) {
                filterChain.doFilter(servletRequest, servletResponse);
                return;
            }

            String openIdConnectProviderName = openIdConnectProviderNames.iterator().next();
            _openIdConnectServiceHandler.requestAuthentication(openIdConnectProviderName, request, response);

        } catch (Exception e) {
            _log.error("Error in KeycloakLoginFilter: " + e.getMessage(), e);
        } finally {
            filterChain.doFilter(servletRequest, servletResponse);
        }
    }

    @Override    public void destroy() {
    }

    @Reference    private OpenIdConnectProviderRegistry _openIdConnectProviderRegistry;
    @Reference    private OpenIdConnectServiceHandler _openIdConnectServiceHandler;

    private static final Log _log = LogFactoryUtil.getLog(KeycloakLoginFilter.class);
}

It intercepts all requests to "/c/portal/login" URL and invokes the OpenID authentication, which redirects user to Keycloak Sign In form.
Check also the article about Servlet Filters.



SLO with Logout Post Action
To make user automatically logged out from Keycloak after logging out in Liferay - a custom Login Post Action may be implemented:
@Component(
        immediate = true,
        property = "key=logout.events.post",
        service = LifecycleAction.class)
public class KeycloakLogoutPostAction implements LifecycleAction {

    @Override    public void processLifecycleEvent(LifecycleEvent lifecycleEvent) throws ActionException {

        try {
            HttpServletRequest request = lifecycleEvent.getRequest();
            HttpServletResponse response = lifecycleEvent.getResponse();

            Collection<String> openIdConnectProviderNames = 
                    _openIdConnectProviderRegistry.getOpenIdConnectProviderNames();
            if (openIdConnectProviderNames == null || openIdConnectProviderNames.size() == 0) {
                _log.warn("No OpenID Connect Providers found.");
                return;
            }

            String openIdConnectProviderName = openIdConnectProviderNames.iterator().next();
            OpenIdConnectProvider openIdConnectProvider = 
                    _openIdConnectProviderRegistry.getOpenIdConnectProvider(openIdConnectProviderName);
            Object oidcProviderMetadata = openIdConnectProvider.getOIDCProviderMetadata();
            String oidcJson = oidcProviderMetadata.toString();
            JSONObject oidcJsonObject = JSONFactoryUtil.createJSONObject(oidcJson);
            Object authEndpoint = oidcJsonObject.get("authorization_endpoint");
            String authEndpointUrl = authEndpoint.toString();
            String logoutEndpoint = StringUtil.replaceLast(authEndpointUrl, "/auth", "/logout");

            String redirectUri = getRedirectUrl(request);

            String logoutUrl = logoutEndpoint + "?redirect_uri=" + redirectUri;
            response.sendRedirect(logoutUrl);

        } catch (Exception e) {
            _log.error("Error in KeycloakLogoutPostAction: " + e.getMessage(), e);
        }
    }

    private String getRedirectUrl(HttpServletRequest request) {
        String portalURL = _portal.getPortalURL(request);
        long companyId = _portal.getCompanyId(request);
        PortletPreferences preferences = _prefsProps.getPreferences(companyId);
        String logoutPath =  _prefsProps.getString(preferences, PropsKeys.DEFAULT_LOGOUT_PAGE_PATH);
        return portalURL + logoutPath;
    }

    @Reference    private Portal _portal;
    @Reference    private PrefsProps _prefsProps;
    @Reference    private OpenIdConnectProviderRegistry _openIdConnectProviderRegistry;

    private static final Log _log = LogFactoryUtil.getLog(KeycloakLogoutPostAction.class);
}
 
This action is invoked after used has been logged out from Liferay. It performs a call to Keycloak's logout endpoint and then redirects back to a portal page.

Enjoy 😉




Comments

  1. Getting null , I am using liferay 7.4.3.11

    ReplyDelete

Post a Comment

Popular posts from this blog

Liferay Search Container Example

Liferay DXP - max upload file size

Liferay Keycloak integration