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.
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.
We 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.
Click to authorize
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.


