Office 365 Authentication in a Liferay

 

Liferay gives us the possibility to add Oauth (OpenId connect) authentication out of the box. It’s also possible to give access to third-party applications via Oauth authentication. But what if you don’t want your user to log in to your portal with Oauth but still give them the possibility to enhance their experience by connecting your portal to some third party service authenticated by Oauth. Following my last post on how to integrate office 365 api in a java web application, I will look at how to integrate that authentication specifically in a Liferay portal.

Handling the Authentication flow

The first thing to do if we want to use Oauth in our portal as a client, we need to handle the authentication flow that we implemented in the preceding post as a servlet filter. Since Liferay support servlet filter, we will simply configure it to answer correctly in our Liferay portal.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
 @Component(
	immediate = true,
	property = {
		"servlet-context-name=",
		"servlet-filter-name=office365-login-filter",
		"url-pattern=/o/o365/login",
	},
	service = Filter.class
 )
 public final class Office365LoginFilter extends BaseFilter {

  The most important part is the url-pattern this tells the servlet when this filter need to run and by having a direct URL, the only thing required to trigger our authentication process is to send a user to that specific URL. If we want to specify the destination page of the user after the authentication process is completed, you have to add the encoded target URL to the backURL parameter. The final URL looks like /o/o365/login?backURL=%2Fweb%2Fguest%2Fo365Loggedin Once the filter received the authentication code, we are calling authenticationService.validateIdToken(authentication, code); to complete the authentication server side.

Keeping the Tokens

It’s great, our user is authenticated on the third party service side and we receive the authentication code. But if we want to access the third party in the future, we need to store the acccess and refresh tokens on our server. This is where the O365Authentication interface become useful :

1
2
3
4
5
6
7
 public interface O365Authentication {
    Serializable getAccessToken();
    String getRefreshToken();
    Instant getAccessTokenExpireAt();
    void setAccessToken(Serializable accessToken);
    String getCallBackURL();
 }

  This interface map all the data that need to be persisted on the server in some way without caring about how it is done. We could use the same interface in a non-Liferay application and provide an implementation of its context and the whole api usage would work on this system without other changes. As we want this to be working on Liferay, we decide to implement this class to persist short term and frequently accessed data to the HTTP session of the user like the access token and the expire of data. The long-term data are stored to Liferay user preferences. I only show the setAccessToken as it’s the one that persists everything, the other methods retrieve the data from the appropriate storage.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 @Override
 public void setAccessToken(Serializable accessToken) {
	httpSession.setAttribute(O365_ACCESS_TOKEN, accessToken);
	
	PortalPreferences userPreference = getUserPreference();
	if(userPreference != null){
		userPreference.setValue(NAMESPACE, REFRESH_TOKEN, getRefreshToken());
	
		Integer expiresIn = getOAuthAccessToken().getExpiresIn();
		Instant expiresAt = Instant.now().plusSeconds(expiresIn-30);
		userPreference.setValue(NAMESPACE, EXPIRES_AT, String.valueOf(expiresAt.getEpochSecond()));
		httpSession.setAttribute(O365_ACCESS_TOKEN+EXPIRES_AT, expiresAt.getEpochSecond());
	}
 }

  There are two things to be careful. First, retrieving the httpSession is different if you have a portletRequest or an httpRequest. You might even have surprised with the httpRequest whether it comes from a filter or from a theme. The constructors take care of those issue :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
 public O365AuthenticationLiferayAdapter(HttpServletRequest request) {
	HttpServletRequest originalServletRequest = PortalUtil.getOriginalServletRequest(request);
	this.httpSession = originalServletRequest.getSession();
	
	StringBuffer requestURL = request.getRequestURL();
	requestURL.delete(requestURL.indexOf("/", 8), Integer.MAX_VALUE);
	requestURL.append("/o/o365/login");
	
	callBackUrl = requestURL.toString();
 }

  The second issue is with the expireIn token. We receive from the provider the number of seconds that the token will be valid and not the time at which it will expire. We have to compute that expiration time when we receive the token. As a precaution, I removed a few seconds from it to request another token if we are too close to the expiration date.

[Bonus] Having the User Login Without Action

In a corporate environment, we tend to have many systems that interact with each other and where the user as to login. Many times we have SSO in place to limit the number of application where we have to login. In particular, with Office365, the user tend to always be logged in if it’s the standard corporate tool to use. So why should we ask our user to manually trigger the authentication flow. We could do a simple post login action to trigger it :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 @Component(
    immediate = true,
    property = {"key=" + PropsKeys.LOGIN_EVENTS_POST},
    service = LifecycleAction.class )
 public final class PostLoginAction extends Action
 {
	@Override
	public void run(HttpServletRequest request, HttpServletResponse response) {
	    try{
	        String redirect = ParamUtil.getString(request, "_com_liferay_login_web_portlet_LoginPortlet_redirect","/");
	        String backUrl = HttpUtil.encodePath(redirect);
	        response.sendRedirect("/o/o365/login?backURL="+backUrl);
	    }catch (Exception e){
	        throw new RuntimeException("Error when redirecting user", e);
	    }
	}
 }

  This redirect our user to the Office 365 login filter with the redirect URL from the Liferay login as the backURL. In this way, every time our user log into our portal, they are also directly authenticated with Oauth 2.0. In an environment where user is most of the time already logged in Office 365, they will only see a white page for a few second and have directly access to all the great integration we provide them!

Final Words

This completes the how to integrate a third party Oauth 2.0 services like Office 365 into Liferay and in java web application in general. The full working code is still available here : https://github.com/savoirfairelinux/office-365-integration. Furthermore, the plugin the push me to write this article is now available on the Liferay marketplace.