Securing The /api/jsonws UI

The one thing I never understood was why the UI behind the /api/jsonws is publicly viewable.

I mean, there's lots of arguments for it to be secured:

  • Exposing all of your web service APIs exposes attack vectors to hackers. Security by obscurity is often one of the best and easiest form of security that you can have1.
  • Just because users may have permission to do something, that doesn't mean you want them to. They might not be able to get to a portlet to delete some web content or document, but if they can get to /api/jsonws and know anything about Liferay web services and parameters, they might have fun trying to do it.
  • It really isn't something that non-developers should ever be looking at.

I'm sorry, but I've tried really hard and I can't think of a single use case where making the UI publicly available is a good thing. I guess there might be one, but at this point it seems like it should just be an edge case and not a primary design requirement.

A client recently asked about how to secure the UI, and although I had wondered why it wasn't secured, I decided it was time for me to figure out how to do it.

The UI Implementation

It was actually a heck of a lot easier than what I thought it was going to be.

I had in my head some sort of complicated machinery that was aware of all locally deployed remote services and perhaps was leveraging reflection and stuff to expose methods and parameters and ...

But it wasn't that complicated at all.

The UI is implemented as a basic JSP-based servlet. Whether in Liferay 6.2 or Liferay 7 CE or Liferay DXP, there are core JSPs in /html/portal/api/jsonws that implement the complete UI servlet code.

For incoming remote web service calls, the portal will look at the request URI - if it is targeting a specific remote service, the request is handed off to the service for handling. However, if it is just /api/jsonws, the portal passes the request to the /html/portal/api/jsonws/index.jsp page for presentation.

Securing the UI

All I'm going to do is tweak the JSP to make sure the current user is an OmniAdmin if they get to see the UI. Nothing fancy, I admit, but it gets the job done. If you have more complicated requirements, you're free to use this blog as a guide but you're on your own for implementing them.

My JSP change is basically going to be wrapping the content area to require OmniAdmin to view the content, otherwise you will see a permission failure. Here's what I came up with:

<div id="content">
	<%-- Wrap content in a permission check --%> <c:if test="<%= permissionChecker.isOmniadmin() %>">
		<div id="main-content">
			<aui:row>
				<aui:col cssClass="lfr-api-navigation" width="<%= 30 %>">
					<liferay-util:include page="/html/portal/api/jsonws/actions.jsp" />
				</aui:col>

				<aui:col cssClass="lfr-api-details" width="<%= 70 %>">
					<liferay-util:include page="/html/portal/api/jsonws/action.jsp" />
				</aui:col>
			</aui:row>
		</div>
	</c:if> <c:if test="<%= !permissionChecker.isOmniadmin() %>"> <liferay-ui:message key="you-do-not-have-permission-to-view-this-page" /> </c:if>
</div>

This code I took mostly from the DXP version of the file, so use the file from the version of Liferay you have so you don't introduce some sort of source issue. I've highlighted the real code that I added so you can work it into your override.

Creating the Project

So regardless of version, we're going to be doing a JSP hook, they just get implemented a little differently.

For Liferay 6.2, it is just a JSP hook. Build a JSP hook and pull in the original /html/portal/api/jsonws/index.jsp file and edit in the change outlined above. I'm not going to get into details for building a 6.2 JSP hook, those have been rehashed a number of times now, so there's no reason for me to rehash it again. Just build your JSP hook and deploy it and you should be fine.

For Liferay 7 CE, well let me just say that what I'm about to cover for DXP is how you will be doing it once GA4 is released.  Until then, the path I'm using below won't be available to you.

For Liferay DXP (and CE GA4+), we'll follow the instructions from https://dev.liferay.com/develop/tutorials/-/knowledge_base/7-0/overriding-core-jsps to override the core JSPs using an OSGi module.

So first I created a new workspace using the command "blade init api-jsonws-override".

Then I entered the api-jsonws-override/modules directory and created my new module using the command "blade create -t service -p com.liferay.portal.jsonwebservice.override api-jsonws-override".

I don't like building code myself, so the first thing I did was add a dependency to a module which has a base implementation of the CustomJspBag in my build.gradle file:

dependencies {
    compileOnly group: "com.liferay.portal", name: "com.liferay.portal.impl", version: "2.0.0"
    compileOnly group: "com.liferay.portal", name: "com.liferay.portal.kernel", version: "2.6.0"
    compileOnly group: "com.liferay", name: "com.liferay.portal.custom.jsp.bag", version: "1.0.0"
    compileOnly group: "org.osgi", name: "org.osgi.core", version: "5.0.0"
    compileOnly group: "org.osgi", name: "org.osgi.service.component.annotations", version: "1.3.0"
}

It's actually that com.liferay.portal.custom.jsp.bag guy which makes my project not work for Liferay 7 CE, it's not available in GA3 but I expect it to be released with GA4. If you don't want to wait for GA4, you can of course not extend the BaseCustomJspBag class like I'm about to and can build out the complete CustomJspBag interface in your class.

NOTE: I really, really dislike the fact that I have to pull in the com.liferay.portal.impl dependency above. I have to do that because for some reason the CustomJspBag interface is only in the portal-impl project and not portal-kernel as we would normally expect.
Do not import this dependency yourself unless you are really, really, really sure that you gotta have it. 95% of the time you're actually going to be wrong, so if you think you need it you really have to question whether that is true or whether you're perhaps missing something.

Since I have the module with the base class, I can now write my JsonWsCustomJspBag class:

package com.liferay.portal.jsonwebservice.override;

import com.liferay.portal.custom.jsp.bag.BaseCustomJspBag;
import com.liferay.portal.deploy.hot.CustomJspBag;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;

/**
 * class JsonWsCustomJspBag: This is the custom jsp bag used to replace the core JSP files for the jsonws UI.
 *
 * @author dnebinger
 */
@Component(
	immediate = true,
	property = {
		"context.id=JsonWsCustomJspBag",
		"context.name=/api/jsonws Permissioning Custom JSP Bag",
		"service.ranking:Integer=20"
	}
)
public class JsonWsCustomJspBag extends BaseCustomJspBag implements CustomJspBag {

	@Activate
	protected void activate(BundleContext bundleContext) {
		super.activate(bundleContext);
	
		// we also want to include the jspf files in the list
		Enumeration enumeration = bundleContext.getBundle().findEntries(
			getCustomJspDir(), "*.jspf", true);
	
		while (enumeration.hasMoreElements()) {
			URL url = enumeration.nextElement();
	
			getCustomJsps().add(url.getPath());
		}
	}
}

Pretty darn simple, huh? That's because I could leverage the BaseCustomJspBag class. Without that, you need to implement the CustomJspBag interface and that makes this code a lot bigger. You'll find help for implementing the complete interface from https://dev.liferay.com/develop/tutorials/-/knowledge_base/7-0/overriding-core-jsps.

Don't forget your override file in src/main/resources/META-INF/resources/custom_jsps/html/portal/api/jsonws/index.jsp file with the override code as discussed previously.

Conclusion

That's it. Build and deploy your new module and you can hit the page as a guest and you'll get the permission message. Hit the page as a non-OmniAdmin user and you get the same. Navigate there as the OmniAdmin and you see the UI so you can browse the services, try them out, etc. as though nothing changed.

I'm making the project available in GitHub: https://github.com/dnebing/api-jsonws-override

 

 

1 A friend of mine called me out on the "security by obscurity" statement and thought that I was advocating for only this type of security.  Security by obscurity should never, ever be your only barrier to prevent folks with malicious intent from hacking your site. I do see it as a first line of defense, one that can keep script kiddies or inexperienced hackers from discovering your remote APIs. But you should always be checking permissions, securing access, monitoring for intrusions, etc.

Blogs
Hi David I totally agree with you, and I think not only /api/jsonws UI shouldn't be publicly available but discover service as well (it needs to reflect changes on Liferay Mobile SDK)
Hi David,

I had this post bookmarked a while back. I could have sworn it was for Liferay 6.2. Did you update the content or am I not remembering correctly? If the former, do you have the documentation for 6.2?
Hey, Chris, I did the blog post using DXP, but most of the concepts will still apply for 6.2. For the PermissionChecker addition, you'd just be building a regular old JSP hook and weaving in your JSP override.