How to leverage Liferay Web Content to integrate a single sign on solution?

When we think about having different authentication types we commonly think about one fluid and intuitive screen to guide the user through this process. And why is that? It’s probably because logging in is a mean to an end, as a user is often trying to pass this step to achieve a broader goal. With this in mind, it is very important to make logging in smoothly and easily.

A logging in process with too many steps can increase the risk of abandonment - and you probably don’t want this into your website. For example, one User Interface Engineering (UIE) study of an online retailer shows that 75% of e-commerce shoppers have never tried to complete their purchase once they have requested their password. And login wall can also be another barrier to users when they’re navigating through websites.

By knowing that, it’s necessary to design a login screen that will make the user experience better for everyone and ensure that no user group is excluded in this step. However, Liferay login module normally adds a few steps to get that done. As this is a very common and important question, I’ve decided to make this post to help you out to get this thing done! As a reminder, this is one way to do this kind of thing. If you have another way, fell free to comment here and I’ll appreciate your suggestions  :)

Open Id connection

As you can have more than one Open Id Provider configured in Liferay, this will be the workflow to login in:

  Which means that you need to click on “OpenId Connect“, choose the provider and then you’ll be redirected to the login page. A lot of steps to log in, right?

What if you want this as a button “Login“ and when you click you go to the login page? A pretty easy way to achieve that is using Web Content + Structure + Template. See the details below:

  1. The login module should be placed in some page. As we normally use Liferay built-in login to login as administrator, we can have an /admin page, for example.

  2. Create the structure below:

{
    "availableLanguageIds": [
        "en_US"
    ],
    "defaultLanguageId": "en_US",
    "fields": [
        {
            "label": {
                "en_US": "OpenID Provider"
            },
            "predefinedValue": {
                "en_US": ""
            },
            "style": {
                "en_US": ""
            },
            "tip": {
                "en_US": ""
            },
            "dataType": "string",
            "indexType": "keyword",
            "localizable": true,
            "name": "OpenIDProvider",
            "readOnly": false,
            "repeatable": false,
            "required": false,
            "showLabel": true,
            "type": "text"
        },
        {
            "label": {
                "en_US": "Login Text"
            },
            "predefinedValue": {
                "en_US": ""
            },
            "style": {
                "en_US": ""
            },
            "tip": {
                "en_US": ""
            },
            "dataType": "string",
            "indexType": "keyword",
            "localizable": true,
            "name": "LoginText",
            "readOnly": false,
            "repeatable": false,
            "required": false,
            "showLabel": true,
            "type": "text"
        }
    ]
}

It’ll be a simple structure with two fields: OpenId Provider and Login Text:

3. Create the template to the structure created in the previous step as below:

<div id="div-openIdConnect">
    <form id="login-openIdConnect" action="/web/guest/admin/-/login/openid_connect_request" method="POST">
        <#-- DIV Form escondida para login com OpenIDConnect-->
        <input 
          type="hidden" 
          name="_com_liferay_login_web_portlet_LoginPortlet_OPEN_ID_CONNECT_PROVIDER_NAME" 
          value="${OpenIDProvider.getData()}">
        </input>
        
        <input 
          class="link-lookalike" 
          id="_com_liferay_login_web_portlet_LoginPortlet_tpvb" 
          type="button" 
          onclick="submitOpenIdForm('login-openIdConnect')" 
          value="${LoginText.getData()}">
        </input>
    </form>
</div>

<script>
    function submitOpenIdForm(form){
        Liferay.fire("loading", {showing:true})
        document.getElementById(form).submit()
    };
</script>

<style>
.link-lookalike {
    background: none;
    border: none;
    color: black;
    cursor: pointer;
}

.link-lookalike:hover {
    text-decoration: underline;
    color: gray;
}
</style>

 

Of course you can change the layout here, add images or whatever you want. You just need to leave the form code and it’ll work.

4. Create the web content indicating the OpenID Connect Provider Name in the first field and the name of the button in the second one, as indicated below:

5. Add this web content in a page, and that’s it! This will redirect the user to the provider login page and it’ll follow the Liferay login workflow without problems.

Of course you can add the structure as a repeatable field to include all OpenID Providers you have.

Social Media connections

What if you want to change the layout on Facebook login in login built-in in Liferay? How can you achieve that? Or what if you want to make it simpler?

Having the Facebook URL to login is a little bit complicated in comparison with OpenID. However, it’s not impossible! You can inject the URL in FreeMarker Templates as additional context variables. It’s necessary to create a Java Class which implements the TemplateContextContributor service, as you can see below:

import java.util.Map;
import javax.portlet.PortletRequest;
import javax.portlet.PortletURL;
import javax.portlet.WindowStateException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

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

import com.liferay.portal.kernel.facebook.FacebookConnect;
import com.liferay.portal.kernel.json.JSONObject;
import com.liferay.portal.kernel.json.JSONUtil;
import com.liferay.portal.kernel.portlet.LiferayWindowState;
import com.liferay.portal.kernel.portlet.PortletURLFactoryUtil;
import com.liferay.portal.kernel.servlet.PortalSessionThreadLocal;
import com.liferay.portal.kernel.template.TemplateContextContributor;
import com.liferay.portal.kernel.theme.ThemeDisplay;
import com.liferay.portal.kernel.util.GetterUtil;
import com.liferay.portal.kernel.util.HttpUtil;
import com.liferay.portal.kernel.util.PropsKeys;
import com.liferay.portal.kernel.util.PropsUtil;
import com.liferay.portal.kernel.util.PwdGenerator;
import com.liferay.portal.kernel.util.Validator;
import com.liferay.portal.kernel.util.WebKeys;

/**
 * @author crystalsantos
 */
@Component(
    immediate = true,
    property = {"type=" + TemplateContextContributor.TYPE_GLOBAL},
    service = TemplateContextContributor.class
)
public class LoginTemplateContextContributor implements TemplateContextContributor {

    @Reference
    private FacebookConnect facebookConnect;
    
    @Override
    @SuppressWarnings("deprecation")
    public void prepare(
            Map<String, Object> contextObjects, HttpServletRequest request) {

        try {
            
            ThemeDisplay themeDisplay = (ThemeDisplay)request.getAttribute(WebKeys.THEME_DISPLAY);

            PortletURL renderUrl =  PortletURLFactoryUtil.create(request, "com_liferay_login_web_portlet_LoginPortlet", PortletRequest.RENDER_PHASE);
            renderUrl.setWindowState(LiferayWindowState.NORMAL);
            renderUrl.setParameter("mvcRenderCommandName", "/login/login_redirect");
            
            String facebookAuthRedirectURL = facebookConnect.getRedirectURL(themeDisplay.getCompanyId());
            String facebookAuthURL = facebookConnect.getAuthURL(themeDisplay.getCompanyId());
            String facebookAppId = facebookConnect.getAppId(themeDisplay.getCompanyId());

            
            HttpSession portalSession = PortalSessionThreadLocal.getHttpSession();
            
            String nonce = null;
            if(Validator.isNotNull(portalSession)) {

                nonce = (String) portalSession.getAttribute(WebKeys.FACEBOOK_NONCE);
                
                if(Validator.isNull(nonce)){
                    nonce = PwdGenerator.getPassword(GetterUtil.getInteger(PropsUtil.get(PropsKeys.AUTH_TOKEN_LENGTH)));
                    portalSession.setAttribute(WebKeys.FACEBOOK_NONCE, nonce);
                }
            }
            
            facebookAuthURL = HttpUtil.addParameter(facebookAuthURL, "client_id", facebookAppId);
            facebookAuthURL = HttpUtil.addParameter(facebookAuthURL, "redirect_uri", facebookAuthRedirectURL);
            facebookAuthURL = HttpUtil.addParameter(facebookAuthURL, "scope", "email");
            facebookAuthURL = HttpUtil.addParameter(facebookAuthURL, "stateNonce", nonce);
            
            JSONObject stateJSONObject = JSONUtil.put(
                    "redirect", themeDisplay.getURLHome()
                ).put(
                    "stateNonce", nonce
                );
            
            facebookAuthURL = HttpUtil.addParameter(facebookAuthURL, "state", stateJSONObject.toString());

            contextObjects.put("facebook_url", facebookAuthURL);

        } catch (WindowStateException e) {
            e.printStackTrace();
        }
    }
}

 

In the code above, line 85 it’s adding a variable called facebook_url which will be available in any FreeMarker Template. This variable will be the Facebook Login URL with all parameters necessary to work in Liferay.

With this, our template will be like this:

<div id="div-openIdConnect">
    <form id="login-openIdConnect" action="/web/guest/admin/-/login/openid_connect_request" method="POST">
        <#-- DIV Form escondida para login com OpenIDConnect-->
        <input type="hidden" name="_com_liferay_login_web_portlet_LoginPortlet_OPEN_ID_CONNECT_PROVIDER_NAME" value="${OpenIDProvider.getData()}"></input>
        
        <input class="link-lookalike" id="_com_liferay_login_web_portlet_LoginPortlet_tpvb" type="button" onclick="submitOpenIdForm('login-openIdConnect')" value="${LoginText.getData()}">
        </input>
        
        
        <a type="button" class="btn ppt-btn ppt-btn--facebook" target="_blank" onclick="facebookRedirect('${facebook_url}')">
            Login with Facebook
        </a>
    </form>
</div>

<script>
    function submitOpenIdForm(form){
        Liferay.fire("loading", {showing:true})
        document.getElementById(form).submit()
    };
    
    function facebookRedirect(url){
        event.preventDefault()
        Liferay.fire("loading", {showing:true})
        location.href = url;
    }
</script>

<style>
.link-lookalike {
    background: none;
    border: none;
    color: black;
    cursor: pointer;
}

.link-lookalike:hover {
    text-decoration: underline;
    color: gray;
}
</style>

All together

By improving our structure and template it’s possible to achieve a more friendly layout, adding the styles of your website or common design to Social Media, for example. You can see an example below:

Updated structure:

{
    "availableLanguageIds": [
        "en_US"
    ],
    "defaultLanguageId": "en_US",
    "fields": [
        {
            "label": {
                "en_US": "OpenID Provider"
            },
            "predefinedValue": {
                "en_US": ""
            },
            "style": {
                "en_US": ""
            },
            "tip": {
                "en_US": ""
            },
            "dataType": "string",
            "indexType": "keyword",
            "localizable": true,
            "name": "OpenIDProvider",
            "readOnly": false,
            "repeatable": false,
            "required": true,
            "showLabel": true,
            "type": "text",
            "nestedFields": [
                {
                    "label": {
                        "en_US": "OpenID Text"
                    },
                    "predefinedValue": {
                        "en_US": ""
                    },
                    "style": {
                        "en_US": ""
                    },
                    "tip": {
                        "en_US": ""
                    },
                    "dataType": "string",
                    "indexType": "keyword",
                    "localizable": true,
                    "name": "OpenIDText",
                    "readOnly": false,
                    "repeatable": false,
                    "required": true,
                    "showLabel": true,
                    "type": "text"
                },
                {
                    "label": {
                        "en_US": "OpenIDIcon"
                    },
                    "predefinedValue": {
                        "en_US": ""
                    },
                    "style": {
                        "en_US": ""
                    },
                    "tip": {
                        "en_US": ""
                    },
                    "dataType": "image",
                    "fieldNamespace": "ddm",
                    "indexType": "text",
                    "localizable": true,
                    "name": "OpenIDIcon",
                    "readOnly": false,
                    "repeatable": false,
                    "required": true,
                    "showLabel": true,
                    "type": "ddm-image"
                }
            ]
        },
        {
            "label": {
                "en_US": "Facebook Text"
            },
            "predefinedValue": {
                "en_US": ""
            },
            "style": {
                "en_US": ""
            },
            "tip": {
                "en_US": ""
            },
            "dataType": "string",
            "indexType": "keyword",
            "localizable": true,
            "name": "FacebookText",
            "readOnly": false,
            "repeatable": false,
            "required": true,
            "showLabel": true,
            "type": "text",
            "nestedFields": [
                {
                    "label": {
                        "en_US": "FacebookIcon"
                    },
                    "predefinedValue": {
                        "en_US": ""
                    },
                    "style": {
                        "en_US": ""
                    },
                    "tip": {
                        "en_US": ""
                    },
                    "dataType": "image",
                    "fieldNamespace": "ddm",
                    "indexType": "text",
                    "localizable": true,
                    "name": "FacebookIcon",
                    "readOnly": false,
                    "repeatable": false,
                    "required": true,
                    "showLabel": true,
                    "type": "ddm-image"
                }
            ]
        }
    ]
}

 

Updated template:

<div id="div-openIdConnect">

    <form id="login-openIdConnect" action="/web/guest/admin/-/login/openid_connect_request" method="POST">
        <div class="btn-group w-100" role="group" aria-label="Login Buttons">
                        
            <#-- DIV Form escondida para login com OpenIDConnect-->
            <input type="hidden" name="_com_liferay_login_web_portlet_LoginPortlet_OPEN_ID_CONNECT_PROVIDER_NAME" value="${OpenIDProvider.getData()}"></input>
            
            <a type="button" class="btn liferay-btn liferay-btn-google" target="_blank" id="_com_liferay_login_web_portlet_LoginPortlet_tpvb" type="button" onclick="submitOpenIdForm('login-openIdConnect')">
                <#if (OpenIDProvider.OpenIDIcon.getData())?? && OpenIDProvider.OpenIDIcon.getData() != "">
                    <img class="liferay-login google-icon" alt="${OpenIDProvider.OpenIDIcon.getAttribute("alt")}" data-fileentryid="${OpenIDProvider.OpenIDIcon.getAttribute("fileEntryId")}" src="${OpenIDProvider.OpenIDIcon.getData()}"/>
                </#if>
            
                <span>${OpenIDProvider.OpenIDText.getData()}</span>
            </a>
            
            <a type="button" class="btn liferay-btn liferay-btn-facebook" target="_blank" onclick="facebookRedirect('${facebook_url}')">
                <#if (FacebookText.FacebookIcon.getData())?? && FacebookText.FacebookIcon.getData() != "">
                    <img class="liferay-login facebook-icon" alt="${FacebookText.FacebookIcon.getAttribute("alt")}" data-fileentryid="${FacebookText.FacebookIcon.getAttribute("fileEntryId")}" src="${FacebookText.FacebookIcon.getData()}" />
                </#if>
                <span>${FacebookText.getData()}</span>
            </a>

        </div>
        
    </form>
</div>

<script>
    function submitOpenIdForm(form){
        Liferay.fire("loading", {showing:true})
        document.getElementById(form).submit()
    };
    
    function facebookRedirect(url){
        event.preventDefault()
        Liferay.fire("loading", {showing:true})
        location.href = url;
    }
</script>

<style>
.liferay-login {
    padding: 20px;  
    border-radius: 5px;
}

.facebook-icon {
    height: 73px;
    width: auto;
    padding-left: 15px;
}

.google-icon {
    height: 70px;
    width: auto;
    padding-left: 15px;
}

.liferay-btn {
    max-height: 50px;
}

.liferay-btn-facebook{
    border-radius: 0px 33px 33px 0px;
    color: #FFF;
    background-color: #255ADE;
    font-size: 17px;
    display: flex;
    align-items:center;
    width: 50%;
}

.liferay-btn-facebook.span{
    margin: auto;
}

.liferay-btn-facebook:hover {
    color: #FFF;
}

.liferay-btn-google{
    border-radius: 33px 0px 0px 33px !important;;
    color: #255ADE;
    background-color: #FFF;
    font-size: 17px;
    box-shadow: 0px 3px 6px rgba(0,0,0, 0.16);
    display: flex;
    align-items:center;
    width: 50%;
}

.liferay-btn-google.span{
    margin: auto;
}

.liferay-btn-google:hover {
    color: #255ADE;
}
</style>

Final result achieved:

As you can see, in this way it’s possible to configure different types of login in one web content, make it easier to change it, add new login types or remove login types. Moreover, you’ll deliver more power to your content team, which will depend less and less on IT teams.

Although you can use the Liferay login without changes, I thought these tips could help your team to be more flexible and independent. Always remember the importance to provide an easy login to your users satisfaction.

I hope you can use the power of web content to improve your environment and design your login in a fluid and easy way to your users. And, of course, if you’ve more tips or doubts, please leave them in the comments below!

Blogs