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.0

Gradle 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:~$ 



Enjoy 😏

Comments

  1. 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

    ReplyDelete
    Replies
    1. HI, Thanks for sharing useful information. Is it possible to access the Liferay Portal Page from external application after passing the accesstoken?

      Delete
  2. HI, Thanks for sharing useful information. Is it possible to access the Liferay Portal Page from external application after passing the accesstoken?

    ReplyDelete
  3. Yes, using authenticated requests you can access the restricted page, see:
    https://lifedev-solutions.blogspot.com/2020/01/making-authenticated-requests-to.html

    ReplyDelete

Post a Comment

Popular posts from this blog

Liferay Search Container Example

Liferay DXP - max upload file size

Liferay Keycloak integration