This website uses cookies to ensure you get the best experience. Learn More.
In this blog I'll explain how to implement a custom Headless API module, configure an OAuth2 authorization and consume the API.
We’ll implement the Headless API for “App manager” functionality with endpoints to get the list of apps, get app details, create/update/delete the app (“App” here is a model, describing some external application with the following fields: appId / name / description / logoUrl / linkUrl).
Inside the Gradle Liferay Workspace create a module for the Headless API with the following structure:
Inside the API folder create the following files:
bnd.bnd - bundle descriptor;
build.gradle - gradle build script file.
Inside implementation folder create the following files:
rest-config.yaml - configuration file for REST Builder;
API bnd.bnd file:
Bundle-Name: Liferay Headless Apps API Bundle-SymbolicName: com.liferay.headless.apps.api Bundle-Version: 1.0.0
Implementation bnd.bnd file:
Bundle-Name: Liferay Headless Apps Impl Bundle-SymbolicName: com.liferay.headless.apps.impl Bundle-Version: 1.0.0
API build.gradle file:
dependencies { compileOnly group: "com.liferay.portal", name: "release.portal.api" }
Implementation build.gradle file:
dependencies { compileOnly group: "com.liferay.portal", name: "release.portal.api" compileOnly project (":modules:liferay-headless-apps:liferay-headless-apps-api") }
Sample rest-config.yaml:
apiDir: "../liferay-headless-apps-api/src/main/java" apiPackagePath: "com.liferay.headless.apps" application: baseURI: "/headless-apps" className: "HeadlessAppsApplication" name: "Liferay.Headless.Apps" author: "Vitaliy Koshelenko" forcePredictableOperationId: false forcePredictableSchemaPropertyName: false generateBatch: false generateGraphQL: false
apiDir - relative path to the api module;
apiPackagePath - java package;
application - information about application;
author - the author name.
The rest-openapi.yaml file is defined according the the OpenAPI Specification and has the following structure:
schema - definition of input and output data types;
info - provides metadata about the API;
openapi - the semantic version number of the OpenAPI Specification version;
paths - holds the relative paths to the individual endpoints and their operations.
The complete rest-openapi.yaml file can be found here.
Use the “buildREST” gradle task (from the implementation module) to generate code for Headless API modules:
The following structure should be generated:
Add “Export-Package” to API’s bnd.bnd to expose generated DTOs/resources:
Headless API modules should be ready for deployment now.
We can run the “deploy” task from the parent “liferay-headless-apps” folder to deploy both API and Implementation modules.
After modules are deployed - we can verify in Gogo shell that they’re up and Active:
Finally, we should be able to find our modules in the Liferay API Explorer (http://localhost:8080/o/api):
At this point we have up and running APIs, but the implementation is still empty. If we look at the generated classes:
we’ll see, that all the methods for APIs are defined in the “AppResource” interface (API module), and they are implemented (with the default implementation) in the “BaseAppResourceImpl” class (implementation module), for example the endpoint for retrieving the list of apps returns just an empty list:
Finally, the “AppResourceImpl” class is the place for introducing the APIs business-logic. We can override a method from the base class, and implement the required logic, sample:
@Override public Page<App> getApps() throws Exception { List<App> apps = appRepository.getApps(); return Page.of(apps); }
In this sample “appRepository” returns just the mocked data from the file. But in a real-world scenario it will probably call the database (with the Service Builder APIs) and convert the fetched entities to the DTOs.
Once we re-deploy the Headless API modules - we should be able to see the results in the API Explorer (by clicking “Execute” for the endpoint):
Headless API endpoints are secured. Although we’re able to execute all the APIs from the API Explorer (without passing any credentials) - this happens because we’re already signed in to the portal as Administrator, and that’s why we have all the sufficient permissions.
To invoke Headless APIs from outside we need to provide the credentials. We can create an OAuth2 Application for this in Control Panel -> Security -> OAuth2 Administration menu:
Here we can define the OAuth2 app name here and set "Client Profile" as "Headless Server".
Once we save the app - clientId and clientSecret should be generated:
On “Scopes” tab we can restrict the OAuth2 app to provide access only to specified Headless APIs:
Finally, we should be able to use OAuth2 app credentials for Headless API authorization.
To perform the authorized API calls we need to get the access token first, using the clientId/clientSecret pair generated by the OAuth2 app.
POST http://localhost:8080/o/oauth2/token
Parameters:
client_id: {clientId} client_secret: {clientSecret} grant_type: client_credentials
Sample (Postman):
In the response we should get the access token value and type, it’s expiration time (in seconds) and the scope (available APIs/actions).
cURL sample:
vitaliy@koshelenko:~$ curl --location --request POST 'http://localhost:8080/o/oauth2/token' --header 'Content-Type: application/x-www-form-urlencoded' --header 'Cookie: COOKIE_SUPPORT=true; GUEST_LANGUAGE_ID=en_US; JSESSIONID=00D6EF23F79BA784AB96CD6B15A2DEC3' --data-urlencode 'client_id=id-404afd28-d74d-0c97-7a3c-3a5ec3867d0' --data-urlencode 'client_secret=secret-5d54762f-ed7f-32c8-1f26-6fe77cbf121' --data-urlencode 'grant_type=client_credentials' {"access_token":"417b526a5f1ff63ff6f4bcfd7794292d1cfafac95b2dde457ab4547c2d1fe","token_type":"Bearer","expires_in":600,"scope":"Liferay.Headless.Apps.everything Liferay.Headless.Apps.everything.read Liferay.Headless.Apps.everything.write"} vitaliy@koshelenko:~$
Once we receive the access_token - we should use it for the subsequent calls for the Headless API (as Bearer Token Authorization).
vitaliy@koshelenko:~$ curl --location --request GET 'http://localhost:8080/o/headless-apps/v1.0/apps' \ > --header 'Authorization: Bearer 1a1ae651371779141ac7afbe5d6f83ef2610899e9dfcf793c4a44991f6784fa' \ > --header 'Content-Type: application/json' \ > --header 'Cookie: COOKIE_SUPPORT=true; GUEST_LANGUAGE_ID=en_US; JSESSIONID=00D6EF23F79BA784AB96CD6B15A2DEC3' \ > --data-raw '{}' { "actions" : { }, "facets" : [ ], "items" : [ { "appId" : "google", "description" : "Google App", "link" : "https://www.google.com", "logoUrl" : "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png", "name" : "Google" }, { "appId" : "facebook", "description" : "Facebook App", "link" : "https://www.facebook.com", "logoUrl" : "https://www.facebook.com/images/fb_icon_325x325.png", "name" : "Facebook" }, { "appId" : "linkedin", "description" : "LinkedIn App", "link" : "https://www.linkedin.com", "logoUrl" : "https://www.linkedin.com/images/logo_300x100.png", "name" : "LinkedIn" } ], "lastPage" : 1, "page" : 1, "pageSize" : 3, "totalCount" : 3 }vitaliy@koshelenko:~$
In this sample we received the list of apps from the custom Headless API, using the OAuth2 authorization.
Enjoy ?