This website uses cookies to ensure you get the best experience. Learn More.
Customizing the OAuth 2 provider GUI for end-users
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.
Topics I will cover in this post...
I will be covering the latter point in the context of scopes relating to JAX-RS applications.
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:
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.
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".
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.
oauth2.scope.
oauth2.application.description.
osgi.jaxrs.name
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.
jaxrs:check
For ScopeDescriptor services you can actually set multiple osgi.jaxrs.name properties on your service to describe scopes from multiple applications using one service.
build.gradle
lb oauth2
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.
Default=true
com.liferay.oauth2.provider.scope.impl
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.
ResourceBundleLoader
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.
src/main/resources/content
Language.properies
Language_XX.properties
bnd.bnd
Provided-Capability
com.example.oauth2.scope.descriptor
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.
resource.bundle.aggregate
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
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
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!).
@RequiresScope("scope")
HTTP GET
GET
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!
HEAD
OPTIONS
POST
PUT
PATCH
DELETE
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! :)