Liferay Headless API Implementation
Liferay Headless APIs Development
Headless APIs: Implementation, Securing and Consuming
Overview
This blog will explain how to implement a custom Headless API module, configure an OAuth2 authorization and consume the Headless REST APIs.
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 (e.g. Google/Facebook), with the following fields: appId, name, description, logoUrl, linkUrl.
Headless Module Creation
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:
bnd.bnd - bundle descriptor;
build.gradle - gradle build script file.
rest-config.yaml - configuration file for REST Builder;
rest-openapi.yaml - OpenAPI definition of REST APIs.
Bundle Descriptors
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.0Gradle Build Scripts
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")
}
REST Builder Config
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.
Note: use forcePredictableOperationId / forcePredictableSchemaPropertyName properties set to false to prevent auto-generated method names (and use the “operationId” property from rest-openapi.yaml file instead).
Note: use generateBatch property set to false to prevent generation of batch processing APIs.
Note: use generateGraphQL property set to false to prevent generation of GraphQL APIs.
OpenAPI Definition
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 sample is shown below:
components:
schemas:
App:
description: An application.
properties:
appId:
description: The application's identificator.
type: string
name:
description: The application's name.
type: string
description:
description: The application's description.
type: string
logoUrl:
description: The application's Logo URL.
type: string
link:
description: The application's link.
type: string
type: object
info:
description: "Liferay Apps Headless API"
license:
name: Apache 2.0
url: http://www.apache.org/licenses/LICENSE-2.0.html
title: Liferay Apps Headless API
version: v1.0
openapi: 3.0.1
paths:
"/apps":
get:
description: Get all apps
operationId: getApps
responses:
200:
content:
application/json:
schema:
items:
$ref: "#/components/schemas/App"
type: array
application/xml:
schema:
items:
$ref: "#/components/schemas/App"
type: array
description: default response
tags: ["App"]
post:
description: Adds a new app
operationId: addApp
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/App"
application/xml:
schema:
$ref: "#/components/schemas/App"
responses:
200:
content:
application/json:
schema:
$ref: "#/components/schemas/App"
application/xml:
schema:
$ref: "#/components/schemas/App"
description: default response
tags: ["App"]
/apps/{appId}:
delete:
description: Removes the app
operationId: deleteApp
parameters:
- in: path
name: appId
required: true
schema:
type: string
responses:
204:
content:
application/json: {}
application/xml: {}
tags: ["App"]
get:
description: Retrieves the app
operationId: getApp
parameters:
- in: path
name: appId
required: true
schema:
type: string
responses:
200:
content:
application/json:
schema:
$ref: "#/components/schemas/App"
application/xml:
schema:
$ref: "#/components/schemas/App"
description: default response
tags: ["App"]
patch:
description: Updates the app
operationId: updateApp
parameters:
- in: path
name: appId
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/App"
application/xml:
schema:
$ref: "#/components/schemas/App"
responses:
200:
content:
application/json:
schema:
$ref: "#/components/schemas/App"
application/xml:
schema:
$ref: "#/components/schemas/App"
description: "App successfully updated"
tags: ["App"]REST Builder Code Generation
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:
Deployment
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):
Business Logic Implementation
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 mocked data from the file. But in a real-world scenario it will probably call the database (with the Service Builder APIs) and convert 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).
Securing Headless APIs with OAuth2
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 these credentials.
We can create an OAuth2 Application for this in Control Panel -> Security -> OAuth2 Administration menu:
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.
Consuming Secured Headless APIs
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 in Postman:
In the response we should get the access token value and type, and it’s expiration time 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).
Sample in Postman:
cURL sample:
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:~$
Good informative post, REST builder is the way to create RESTful apis in DXP and this post has all details captured to let us know all of that.Maybe we can create a post where we mention the open api schema details using various data types like array ,long,enum etc , that will help others too when they code the schema yaml
ReplyDeleteHI, Thanks for sharing useful information. Is it possible to access the Liferay Portal Page from external application after passing the accesstoken?
DeleteHI, Thanks for sharing useful information. Is it possible to access the Liferay Portal Page from external application after passing the accesstoken?
ReplyDeleteYes, using authenticated requests you can access the restricted page, see:
ReplyDeletehttps://lifedev-solutions.blogspot.com/2020/01/making-authenticated-requests-to.html