Securing Headless API Calls in Liferay DXP

Best Practices for Fragments and Custom Elements Client Extensions

Calling Liferay DXP Headless APIs (REST or GraphQL) from the frontend using Fragments, Custom Elements, or Client Extensions is easy and powerful.

But there’s a hidden danger: credentials are often exposed in the browser, leaving APIs vulnerable to misuse.

In this article, I’ll walk you through:

  • Why embedding credentials (Basic Auth or OAuth2.0 Client Secret) in frontend code is insecure.

  • How I implemented secure approaches in React/Angular Client Extensions and Fragments.

The Problem: Exposed Credentials in the Browser

When calling headless APIs, many implementations put credentials in the frontend code.

1. Basic Auth Example

fetch('/o/headless-delivery/v1.0/sites/1234/blog-postings', {
  headers: {
    "Authorization": "Basic YWRtaW46YWRtaW4=" // exposed in browser
  }
});

Anyone can open Developer Tools → Network/Console and see the Authorization header.

2. OAuth2.0 Client Credentials Flow in Frontend

Sometimes developers use OAuth2.0 Client Credentials directly from frontend code:

const response = await fetch("/o/oauth2/token", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: "grant_type=client_credentials&client_id=myClientId&client_secret=mySecret"
});
const token = await response.json();

Problems:

  • Client ID & Secret are visible in JavaScript code.

  • Browser console or network tab shows the full request.

  • Anyone can reuse the client credentials to generate tokens outside your app.

This completely breaks the security model of OAuth2.0

Why This Matters

  • Data Leakage: PII or sensitive data in APIs could be exposed.

  • Credential Theft: Attackers can reuse tokens or secrets.

  • Non-Compliance: Financial and healthcare applications must follow strict security practices.

The Secure Approach

Instead of exposing credentials in frontend code, you should:

1. Use Liferay.authToken in Fragments

 

return fetch(window.location.origin + '/' + url, { 

headers: { 

'Content-Type': 'application/json', 

'x-csrf-token': Liferay.authToken, 

}, 

...options, 

}); 

You can refer below sample workspace provided by Liferay. 


2. Use Liferay.authToken in custom element client extension in my case, I am use react client extension .


 

If you look at the above screenshot headless call is getting managed using api() which is handling authentication. If you look at below sample workspace client extension project, api.js and liferay.js files handling the authentication logic.

https://github.com/liferay/liferay-portal/blob/master/workspaces/liferay-sample-workspace/client-extensions/liferay-sample-custom-element-2 

There is one catch though if you are making this headless call from non-logged in context some APIs may throw 403 forbidden error on browser console. 

To make this working you will have to allow API you are calling from service access policy. Navigate to Control Panel → Security → Service Access Policy. 
 
https://learn.liferay.com/w/dxp/security-and-administration/security/securing-web-services/setting-service-access-policies 

 

3. Use OAuth2.0 authorization protocol. 

If you must use OAuth2.0, never call the token endpoint directly from the browser. Instead:

  1. Create a backend endpoint (Spring Boot, Node, etc.) that handles the token exchange securely.
    Example: Spring Boot Endpoint Sample

  2. From your React app, use Liferay.OAuth2Client to call APIs with valid tokens:

let oAuth2Client;

try {

oAuth2Client = Liferay.OAuth2Client.FromUserAgentApplication( 'liferay-sample-etc-spring-boot-oauth-application-user-agent' );

} catch (error) { console.error(error); }

Example React Implementation: DadJoke.js

 

Additional security aspect that you can implement


1. Configure CORS in Liferay so your frontend (fragment, client extension) can make authenticated calls.
2. You can write proxy filter to making it secure by checking

  • Frontend calls your proxy endpoint (in Liferay or external server).

  • Proxy injects the required authentication (Basic, OAuth2.0 token).

  • Credentials stay server-side only.

Key Takeaways

  • Don’t use Basic Auth or OAuth2.0 Client Credentials flow in frontend.

  • Use Liferay session authentication when possible.

  • For external APIs, use a backend proxy to handle tokens securely.

  • Always validate user permissions server-side.

Conclusion

Hardcoding credentials in frontend code is not just bad practice — it’s a serious security risk.

By using Liferay.authToken, leveraging service access policies, and delegating OAuth2.0 flows to backend services, you can keep your Liferay Headless API calls secure while still building modern, composable experiences.

Blogs

Thanks for highlighting and offering the solution.