OAuth Client Portlet Implementation

Hi all, recently our team finished the first real-world OAuth case, so I'll share with you code and knowledge you would probably need if you choose Liferay OAuth Plugin.

Architecture

Our case requires two kind of portals:

  • Liferay Service Portal - further in text LSP - EE 6.1.x portal - this is central point where resources and services are being served from. Here we have resource administrator users, and end users (mostly "consumer" portal administrators). Here OAuth Portlet was deployed.
  • Liferay Client Portal - further in text LCP - any EE/CE 6.1.x/6.2.x portal - this is portal instance that consumes particular resource (imagine it like your portal instance accessing Marketplace to fetch an portlet, but with more resources and services). Here OAuth Client Portlet was deployed

LCP instances are consumers, consuming resources and services provided by LSP. Consuming is done with simple portlet (we refer to it as OAuth Client Portlet) whose threads wait in the background and from time to time does some work with LSP. Each request for service or resource must be authorized, but without human interference.

Note that I use term OAuth Portlet which is portlet plugin that makes particular portal instance OAuth friendly, and term OAuth Client Portlet which is portlet plugin that performs authorized requests using OAuth principles.

Authorization problem

LCP instances need way to authenticate and authorize its actions at LSP. It could be done using username/password with each request, but no one administrator wants his username and password in properties or database (even encrypted).

As we already have OAuth Portlet which setups an Liferay Portal to be OAuth friendly, we decided to go further with OAuth.

OAuth Client Portlet registered as OAuth ApplicationWe had deployed OAuth Portlet at LSP portal and registered our OAuth Client Portlet as OAuth Application to obtain CONSUMER KEY and CONSUMER SECRET. With consumer credentials we were ready to implement clent side. Further in text you can read how we did it, and what are limitations related to current OAuth Portlet implementation (don't panic we are working on improvements).

OAuth Client Portlet Implementation

There are so many code examples at Internet on how to use OAuth. Here I'll describe how we did it in portlet and what was different comparing to some other cases (for example if you build mobile application).

OAuth Client Library

We choose Scribe as OAuth library of our choice. Simply because it is available in portal and you can easily include it in your plugin by refering it from liferay-plugin-package.properties:

portal-dependency-jars=\
    httpcore.jar,\
    ...,\
    json-java.jar,\
    scribe.jar

Current version available in portal is pretty old 1.0.8 but it would be quite enough for our portlet plugin.

Scribe OAuth Service implementation

The first step is Scribe's OAuth service implementation. Here is code:

public class OAuthAPIImpl extends DefaultApi10a {

	@Override
	protected String getAccessTokenEndpoint() {
		if (Validator.isNull(_accessTokenEndpoint)) {
			_accessTokenEndpoint = OAuthUtil.buildURL(
				"oauth-portal-host", 80, "http",
				PortletPropsValues.OSB_LCS_PORTLET_OAUTH_ACCESS_TOKEN_URI);
		}

		return _accessTokenEndpoint;
	}

	@Override
	protected String getRequestTokenEndpoint() {
		if (Validator.isNull(_requestTokenEndpoint)) {
			_requestTokenEndpoint = OAuthUtil.buildURL(
				"oauth-portal-host", 80, "http",
				PortletPropsValues.OSB_LCS_PORTLET_OAUTH_REQUEST_TOKEN_URI);
		}

		return _requestTokenEndpoint;
	}

	private String _accessTokenEndpoint;
	private String _requestTokenEndpoint;

}

In latest version of Scribe you will need to implement getAuthorizeToken method also. It is starting step for client accessing OAuth platform the first time, and here it is implemented in OAuthUtil class. By implementing this we provide Scribe with informations about OAuth platform we are accessing. It is very important to say that OAuth Portlet protocol URLs are defined in portal.properties and liferay-hook.xml. Defaults are

auth.public.paths=\
    /portal/oauth/access_token,\
    /portal/oauth/authorize,\
    /portal/oauth/request_token

and in our example we use them as they are. At an LSP you are connecting these might be modified, so in your case consult with provider portal administrator if these properties were modified. We use portlet.properties to set all OAuth related constants:

oauth.access.token.uri=/c/portal/oauth/access_token
oauth.authorize.uri=/c/portal/oauth/authorize?oauth_token={0}
oauth.consumer.key=42c56e22-d5a2-4003-86f4-cbc34b6de3e3
oauth.consumer.secret=793195c2936a85649042b24ed843a036
oauth.request.token.uri=/c/portal/oauth/request_token

and finally OAuthUtil class to implement what we were missing:

public class OAuthUtil {

	public static String buildURL(
		String hostName, int port, String protocol, String uri) {
	 ...
	}

	public static Token extractAccessToken(
		Token requestToken, String oAuthVerifier) {

		Verifier verifier = new Verifier(oAuthVerifier);

		OAuthService oAuthService = getOAuthService();

		return oAuthService.getAccessToken(requestToken, verifier);
	}

	public static String getAuthorizeURL(
		String callbackURL, Token requestToken) {

		if (Validator.isNull(_authorizeRequestURL)) {
			authorizeRequestURL = buildURL(
			"oauth-portal-host", 80, "http",
			PortletPropsValues.OSB_LCS_PORTLET_OAUTH_AUTHORIZE_URI);

			if (Validator.isNotNull(callbackURL)) {
				authorizeRequestURL = HttpUtil.addParameter(
					authorizeRequestURL, "oauth_callback",
					callbackURL);
			}
		}

		_authorizeRequestURL.replace("{0}", requestToken.getToken());
	}

	public static OAuthService getOAuthService() {
		if (_oAuthService == null) {
			ServiceBuilder oAuthServiceBuilder = new ServiceBuilder();

			oAuthServiceBuilder.apiKey(
				PortletPropsValues.OSB_LCS_PORTLET_OAUTH_CONSUMER_KEY);
			oAuthServiceBuilder.apiSecret(
				PortletPropsValues.OSB_LCS_PORTLET_OAUTH_CONSUMER_SECRET);
			oAuthServiceBuilder.provider(OAuthAPIImpl.class);

			_oAuthService = oAuthServiceBuilder.build();
		}

		return _oAuthService;
	}

	public static Token getRequestToken() {
		OAuthService oAuthService = getOAuthService();

		return oAuthService.getRequestToken();
	}

	private static String _authorizeRequestURL;
	private static OAuthService _oAuthService;

}

Please take moment and stop at method String getAuthorizeURL(String callbackURL, Token requestToken). This method is very related to CALLBACK URI parameter you are obligatory to provide during registration of an OAuth Application. CALLBACK URI is uri where user will be redirected by LSP once user is authenticated and access authorized. For an mobile application, or 3rd party web application with known domain we can use CALLBACK URI as my-android-app://main-activity or http://www.consumersite.com/registered. In our example, consumer portal domain in build time is unknown, or LCP portal could be behind firewall and admin referrs to it via IP or internal alias when performing OAuth authorization. To support case, we need to override CALLBACK URI and hopefully we can do it with additional oauth_callback parameter as shown in example. Remeber this if you plan your own OAuth case.

Authentication

If accessing OAuth platform first time, portlet needs UI to start OAuth cycle which would result in ACCESS TOKEN and ACCESS SECRET. Those are credentials we will store for further usage. Portlet will use them each time we are accessing LSP (in combination with CONSUMER KEY and CONSUMER SECRET). Our front end code works this way:

  • if there are ACCESS TOKEN and ACCESS SECRET show threads activity status
  • otherwise tell user he/she needs to authorize portlet with target OAuth Platform

This is code sample in JSP which initiate OAuth authorization process:

<portlet:actionurl name="setupOAuth" var="setupOAuthURL">
<%
Token requestToken = OAuthUtil.getRequestToken();

portletSession.setAttribute(Token.class.getName(), requestToken);
%>
<div class="button-container"%>
	<a class="lcs-portal-link" href="<%= OAuthUtil.getAuthorizeURL(setupOAuthURL, requestToken) %>"><liferay-ui:message key="authorize-access"/>
	</a>
</div>

After Authorize button is clicked, user is taken to OAuth Provider where he is authenticated and asked to authorize Portlet to access resources and services on his behalf.

OAuth Client Portlet shows to user authorize buttonClick to authorizeOAuth Portlet Requests user to authorize access

On success, OAuth platform redirects user back to CALLBACK URL (where you want your portlet to continue with process). In our case it is portlet action setupOAuth where we will extract and persist access token and access secret.

public void setupOAuth(
		ActionRequest actionRequest, ActionResponse actionResponse)
	throws Exception {

	PortletSession portletSession = actionRequest.getPortletSession();

	Token requestToken = (Token)portletSession.getAttribute(
		Token.class.getName());

	String oAuthVerifier = ParamUtil.getString(
		actionRequest, "oauth_verifier");

	Token token = OAuthUtil.extractAccessToken(requestToken, oAuthVerifier);

	// store token.getSecret() and token.getToken()
}

Once we have access token and secret our threads can use it for background communication with LSP. In our case I'm accessing services via JSON WS at LSP. Here is simple example how you can do it:

Token token = new Token(getAccessToken(), getAccessSecret());

String requestURL = OAuthUtil.buildURL(
	"oauth-portal-host", 80, "http",
	"/api/secure/jsonws/context.service/method/parms");

OAuthRequest oAuthRequest = new OAuthRequest(Verb.POST, requestURL);

OAuthService oAuthService = OAuthUtil.getOAuthService();

oAuthService.signRequest(token, oAuthRequest);

Response response = oAuthRequest.send();

if (response.getCode() == HttpServletResponse.SC_UNAUTHORIZED) {
	String value = response.getHeader("WWW-Authenticate");

	throw new CredentialException(value);
}

if (response.getCode() == HttpServletResponse.SC_OK) {
	// do something with results from response.getBody();
}

Liferay OAuth Portlet Limitations

Please DON'T consider this case as ultimate one. This can be pattern for most projects where client is built for known Liferay Portal Platform (let's say Liferay.com, or any other amoung our respected customers). If you build Portlet Plugin or Mobile Application for "Any Liferay Portal" OAuth won't work for you. It won't work for you since you will need to feed your code with CONSUMER KEY and CONSUMER SECRET which in "Any Liferay Portal" case will be known after "Any Liferay Portal" was installed and OAuth Plugin is deployed. For mobile applications downloaded from Play, App Store or Marketplace it will be very hard to fetch these in easy and secure way.

Please, prior considering using OAuth Portlet take note that current OAuth Portlet implementation relys to standard portal permissions. That means if user Europa authorizes portlet Atlass to access LSP portal resources, portlet Atlass will be able to do what ever Europa can do. In next version of OAuth Portlet, we will engage Access Control API where consuming applications would be forced to declare which resources will access, and OAuth Portlet would check it.

This is The End (of blog article)

At the end, I hope it will be very easy to get your OAuth clients to life. Also I expect lot of critics mostly because of limitations, but hey, we are working to improve it.

Blogs
Need few clarifications. Is user logged into LCP different from user who authenticates at LSP during authorization? in other words, is oauth used to login to LCP?
Hi Sameer, given example is quite confusing since we have 2 Liferay portals. If LCP belongs to Company1 and LSP to Company2 it is more likely that user databases will differ, and User credentials used to login to LCP are not same as those used for LSP. However if LCP and LSP belong to same company, and User databases are synced via LDAP than yes, those are same users.
OAuth is not used to login, I see it more likely as a way to authorise 3rd party device/machine/site to access particular resources without storing your credentials (user name/pass) at 3rd party device/machine/site.
Hope this helps?
Hi Igor,

We are planning to configure the third-party Authentication provider to handshake with the Liferay Portlet. Did we have any suggestions how we can proceed to integrate with the third-party Authentication provider which using OAuth with Liferay

Did you have the OAuth Client Portlet code available in git ?
HI Sharavan, this link could help you:
https://dev.liferay.com/develop/tutorials/-/knowledge_base/6-2/authorizing-access-to-services-with-oauth

Complete AOauth consumer portlet code is available only for customers with subscriptions.
Need some clarification regarding the below use case:

As per my use case the Oauth token provider in the third party application which will be authorizing the user and the user credentials are also not stored in the same database and i need to build the consumer of oauth token on liferay and authorize the user after successfully verifying the token.
Can this plugin support this use case for consuming the oauth token and authorizing the user without having the password stored at same database with only having the emailAddress value in the token so that we can identify the user in the system to verify this user exists and on the basis of trust based on token the user can login into Portal and access the application the same way it access the application while directly login into portal.

Please suggest, as i am very new to oauth way of accessing the application.
Hi Divya, this article gives you guidelines how to develop portlet whose requests to access remote resources are authorized via OAuth. In this example Liferay OAuth Provider plugin is what you call "third party application which authorizes the user". If I understand your case, you would like to authenticate user against Twitter, Facebook or Google using OAuth... Yes you can do it but this article doesn't cover your case.
how can we get logged in user details after successfully user authentication by token like screenname and userid ?
Hi Kalpesh, OAuth is designed so that Consumer APP never needs user details to perform actions on user's behalf. If you really need user name you can/should either use remote call to a service that will serve current user details (for example extend UserServiceImpl) or make your application to ask user to provide some details. Summary: OAuth spec doesn't require implementation to reveal user's human-friendly identity to Consumer APP.