Customizing the OAuth 2 provider GUI for end-users

OAuth 2 is built on end-user trust, be precise in what you are asking of them

Liferay Portal provides comprehensive OAuth 2 support. It can be used to secure both the JSON-WS and JAX-RS APIs, and very little effort on behalf of the developer is required to support it for 3rd party modules also. In fact, simply deploying a standard JAX-RS app into Liferay Portal is sufficient to enable it! Such auto-magic is incredibly useful, but can be a bit unsettling also when you are trying to keep a close eye on how your portal presents itself to the world. The topic of this post is how to customize the general presentation of information provided to the end-users through the OAuth 2 provider in Liferay Portal.

First of all, DON'T PANIC, though all deployed JAX-RS apps are automatically protected by OAuth 2, they cannot be accessed using OAuth 2 until you create an OAuth 2 application via the control panel, and assign it the appropriate scopes. Liferay Inc. follows the "Secure by Default" principle for all developments. 

Topics I will cover in this post...

  • Visual presentation of the Authorization request portlet
  • Visual presentation of OAuth 2 scopes to OAuth 2 administrators & end-users

I will be covering the latter point in the context of scopes relating to JAX-RS applications.

Visual presentation of the Authorization request 

This prompt is rendered whenever a 3rd party application wants to seek authorization from the end-user to access Liferay API on their behalf.  So in most end-user interactions this is the first and only visual representation of Liferay Portal. Meaning this is where you need to mentally link to that established trust that your portal has with the end user. Otherwise they will not feel comfortable in authorizing the access request.

By default, the prompt is provided by a runtime portlet, in maximized state, on the home page of the default site. i.e. http://yourdomain:8080 . This means it will pick up the theme that is assigned to this site.  It is not currently possible for the portlet to render in normal state, meaning the selected theme  is the only content delivery mechanism (not counting the dynamic text of the authorization request itself). Of course you can embed content in the theme, but I will not cover that topic in this post as it is already covered well elsewhere.

You do however have control over the URL used. The recommended way to change this is:

  1. Create a new "Full page application" type page with desired URL and select the "Application Authorization Request" portlet
  2. Go to Instance Settings >> OAuth 2 >> Authorize Screen. Change "Authorize Screen URL" to the URL of the page.

It is actually possible to use URLs that do not resolve to the portal here, but then you will need to implement the full UI and also callback to the portal to sign the authorization decision. It's an advanced topic which I can cover in a future post if there is demand.

Visual presentation of OAuth 2 scopes to OAuth 2 administrators & end-users

Now this leads naturally to customizing how the authorization request text itself is composed. 
By default, the text focusses simply on differentiating between read and write type access. You'll see prompts like ...

"For Forms, read data on your behalf."

(version 7.2+, it might be different in earlier versions).

This sentence is actually constructed by two OSGi services.

  • ApplicationDescriptor
  • ScopeDescriptor

ApplicationDescriptor provides "For Forms" , and ScopeDescriptor provides "read data on your behalf".

Customizing JAX-RS applications you maintain yourself

These services are automatically registered by the runtime whenever you deploy JAX-RS applications. This means that all you need to do is add language keys to your application module's resource bundle. All keys for scopes must start with "oauth2.scope." and end with the scope name. The application description key must start with "oauth2.application.description." and end with the value of the JAX-RS Application's osgi.jaxrs.name service property.

Customizing Liferay bundled JAX-RS applications

But what if you want to override the text relating to modules you cannot modify the source code for? Then you can register overriding ApplicationDescriptor and/or ScopeDescriptor OSGi services with a osgi.jaxrs.name service property matching the existing JAX-RS app you want to override. You can find the osgi.jaxrs.name by executing "jaxrs:check" from the GoGo shell.

For ScopeDescriptor services you can actually set multiple osgi.jaxrs.name properties on your service to describe scopes from multiple applications using one service.

You will need to add a Gradle dependency to your module's build.gradle as shown below. But make sure to replace "X" with the major version of the SPI module as found if you execute " lb oauth2" from GoGo shell.

compileOnly "com.liferay:com.liferay.oauth2.provider.scope.spi:X.0.0"

One thing worth mentioning is that Liferay's OAuth 2 provider publishes a default ScopeDescriptor OSGi service (it has "Default=true" service property) for whenever a JAX-RS application module is missing a language key demanded by one of its scopes. The resource bundles for these translations are provided by the com.liferay.oauth2.provider.scope.impl module.

In fact, it is rare that Liferay's bundled JAX-RS applications provide translations for scopes, meaning these defaults represent our convention. So you can change a lot by simply changing this default service, or if you prefer, the ResourceBundleLoader service that the default service uses. This can be achieved using Liferay's Language Extender functionality, which allows to publish an overriding ResourceBundleLoader service for any module.

To do so simply create a new module with a src/main/resources/content directory containing a Language.properies with the keys you want to override, plus a Language_XX.properties file for each locale you want to provide a translation for. Then modify the module's bnd.bnd to include the "Provided-Capability" header below, replacing "com.example.oauth2.scope.descriptor" with your module's symbolic name.

Provide-Capability:\
    liferay.resource.bundle;\
        resource.bundle.aggregate:String="(bundle.symbolic.name=com.example.oauth2.provider.scope.spi.scope.descriptor.convention),(bundle.symbolic.name=com.liferay.oauth2.provider.scope.impl)";\
        resource.bundle.base.name="content.Language";\
        bundle.symbolic.name="com.liferay.oauth2.provider.scope.impl";\
        service.ranking:Long="2"

p.s. The resource.bundle.aggregate line can be removed if you intend to provide all the keys, as it simply re-publishes the original keys as defaults.

You can use the original Language.properties as a reference for what keys to override. Remember, all keys demanded by scopes start with "oauth2.scope." and end with the scope name.

You can find a complete example here: https://github.com/stian-sigvartsen/example-oauth2-provider-scope-convention

You can also use this approach to override the ResourceBundleLoader of specific JAX-RS applications if prefer not to publish a service in code.

Can I achieve all custom translations using a single custom OSGi service?

Yes, well no, but it can be achieved with a single Java class at least! You can find a comprehensive example of an OSGi component that automatically registers overriding OSGi services (both ApplicationDescriptor and ScopeDescriptor) for all JAX-RS applications, here: https://github.com/stian-sigvartsen/example-oauth2-provider-scope

 

Final words...

When you look at the default language keys you will notice that there is some duplication in descriptions. This is because when JAX-RS apps are deployed without any @RequiresScope("scope") annotations, the HTTP verbs of the application are used to create scopes named by the HTTP verbs. The thing is you really can't expect end-users authorize HTTP GET permission to some 3rd party. It means nothing to them! Even if they are slightly technical and appreciate what HTTP GET means, they have no idea how GET is used in the context of a particular JAX-RS application (there are many horror stories!).

So given this situation, the same translation is provided for GET,HEAD and OPTIONS. And also POST,PUT,PATCH and DELETE. Don't worry, at runtime the scope descriptions are de-duplicated whenever appropriate!

So that is enough for this blog post I think. If you got this far then you are probably eager to learn about Liferay's OAuth 2 capabilities, and you might have an idea for a follow up topic, or something that needs more clarification. Please let me know in the comments section! :)