Building Headless APIs

Headless APIs: generation, implementation, securing and consuming

Overview

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

Headless API Modules Implementation

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 and 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 can be found here.

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 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):


 

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

Consuming Secured Headless APIs

Request the Access Token

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

Bearer Token Authorization

Once we receive the access_token - we should use it for the subsequent calls for the Headless API (as Bearer Token Authorization).

Sample (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:~$

In this sample we received the list of apps from the custom Headless API, using the OAuth2 authorization.

 

Enjoy ?

Blogs

Hello Vitaly, how can i expose a rest service which take the userid as parameter and return true/false based on if the user has a certain role? ( i use portal session token x-csrf as authentication method). Can you give me an example? Thank you in advance