Blogs
Choose the right OAuth 2.0 Authorization Flow For Your Application.
Introduction
I've recently started working on a React SPA to take advantage of the Liferay Headless APIs. I was working through all of my implementation details and was finally ready to start making API calls, but I needed to figure out how to handle authenticated requests.
I reached the following point in the documentation, https://portal.liferay.dev/docs/7-2/frameworks/-/knowledge_base/f/making-authenticated-requests#oauth-20-authentication and I went ahead and implemented the client credentials authorization flow and was happily retrieving web contents when a thought struck me...
What if I wanted to author a web content article?
I quickly realized that the Client Credentials authorization flow is not going to be the best type in all cases. I also didn't find any guidance in the documentation how to pick the right authorization flow, so I thought I'd pen a quick blog to help you choose the best option for you.
OAuth 2.0 Authorization Flows
Liferay supports four different authorization flows:
- Authorization Code Flow
- PKCE Extended Authorization Code Flow
- Client Credentials Authorization Flow
- Resource Owners Authorization Flow
Each of these authorization flows are different, but they all have the same result: they return an Access [Bearer] Token. This token gets submitted with each headless API request (or /api/jsonws request or classic REST request) and will be used to allow access to the API endpoints.
In each of the flows, you will be using your registered client ID (you get a client ID and sometimes a client secret code when you register your application in the OAuth 2 Administration control panel) as part of the request parameters when asking for the Access Token.
Let's take a look at each of the flows and identify their pros and cons and recommended use cases.
Resource Owners Authorization Flow
The Resource Owners Authorization Flow is an infrequently used flow and not suggested at all for SPAs.
With the Resource Owners flow for getting an access token include passing the username and password in clear text with the token request. Liferay shares the following example for an access token request using the Resource Owners flow:
https://[hostname]/o/oauth2/token ?grant_type=password &client_id=[client ID] &client_secret=[client secret] &username=[user@emailaddress.com] &password=[password]
Although I broke this up across multiple lines, the request itself would be one single URL.
This request will give you an access token, but as you can see it leaks your username and password in the URL itself. That's way too insecure for a SPA to expose yours or anyone's credentials.
| Pros | Cons |
|---|---|
| Easy to see who is authenticating. | Exposes username and password in cleartext. |
| Single request to receive an access token. | Even if using HTTPS, the URL itself is not protected by SSL. It truly is cleartext. |
| The user passed is the user authenticated on Liferay's side. | May require user interaction to collect user credentials. |
| No backend interaction for authorized access. |
When is this this authorization flow a good choice? Never, if you ask me. The cleartext exposure of username and password is at too great a risk of being intercepted and used in ways you would never approve.
I guess if you had a secured app, well away from public access, inside of your organization but protected by layers of firewalls and security to prevent hacker access, maybe you might be safe leveraging this kind of authorization flow, but generally I can't see this being appropriate for any public use, especially for a SPA.
Client Credentials Authorization Flow
Although this authorization flow is also infrequently used, it is the flow suggested in Liferay's documentation introducing using the new Headless APIs covered here: https://portal.liferay.dev/docs/7-2/frameworks/-/knowledge_base/f/making-authenticated-requests#obtaining-the-oauth-20-token
Liferay's example for the Client Credentials flow is:
https://[hostname]/o/oauth2/token ?grant_type=client_credentials &client_id=[client ID] &client_secret=[client secret]
| Pros | Cons |
|---|---|
| Simplest request w/o leaking details. | Cannot represent different users. |
| Permissions totally controlled by server side. | Cannot represent different access levels. |
| No backend interaction for authorized access. |
The key part of the client credentials flow is that the credentials to use are determined and set by the application owner, the person that registers the application in the OAuth 2 Administration control panel.
In my React SPA, I'm using Axios and QS to make this call to retrieve the access token; the method I use is:
export const requestClientCredentialsAccessToken = (baseUrl, clientId, secret) => {
const params = {
client_id: clientId,
client_secret: secret,
grant_type: 'client_credentials',
};
return axios.post(`${baseUrl}/o/oauth2/token`, qs.stringify(params), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
});
};
When defining an application that uses the client credentials flow, the admin will select the user that anyone using the client ID and secret will be impersonating. You can select a system admin (not recommended) down to simple guest access.
So this selection is key; you want to pick a user to impersonate that has the necessary access to Liferay services, but no more than what the application is going to need. Anyone with the client id and secret can request a token and then start calling headless APIs, /api/jsonws or classic REST APIs. So you want to ensure that the selected user doesn't have access to anything outside of what the SPA application needs in case you lose control of the client IDs/secrets.
When is this type of authorization flow useful? I would say if you are doing read-only access to portions of Liferay, this flow is for you. There's no need to gather user credentials, no need for interacting with the backend for authorization, and you can grab a token and start using it for read-only requests.
It is not going to be good, however, for data creation, update or deletion, for auditing purposes (who is viewing what), or supporting different levels of access depending upon user privileges. The user that is impersonated as (designated in the OAuth 2 Admin control panel entry for the app), that is the access the user has, any creation/change/delete can only be stamped with that user, and for audit purposes it will appear like this user is doing everything (because every incoming API request is impersonating that user).
But if you have a custom SPA with your own logic, using your own datastore (outside of Liferay), and the only things you want to do is pull in content from Liferay to display in your SPA? I think client credentials will be a super easy and effective way to do this, but you must take care when selecting the user to impersonate.
Authorization Code Flow
Authorization code flow is one of most frequently used methods for OAuth 2, especially in web applications.
This flow operates in two steps. The first step is the request for authorization. The Liferay example URL for this is:
https://[hostname]/o/oauth2/authorize ?response_type=code &client_id=[client ID]
The twist is that this is not sent as a background request, this is a redirect sent to the OAuth 2 provider. For Liferay, the user is redirected to a login page and after that the user will see a dialog requesting authorization for the application (as entered in the OAuth 2 Admin control panel).
If the user authorizes the app, the browser is redirected back to the outside app (the redirect URL is entered in the OAuth 2 Admin control panel) and includes a code generated by the server.
The application can then issue a POST request for an access token, including the code, with a URL similar to Liferay's example:
http://localhost:8080/o/oauth2/token
No URL parameters with this one, instead the request body will be x-www-form-urlencoded with the following parameters:
client_id=[client ID] client_secret=[client secret] grant_type=authorization_code code=[authorization server generated code] redirect_uri=[registered callback URI]
Liferay will generate an access token and return it in the response body for the submission.
Along with the access token, you'll also get a refresh token. The refresh token can be used after the access token expires, to request a new access token without going through the full authorization process again.
| Pros | Cons |
|---|---|
| No need to capture or know user credentials. | Redirects to Liferay for authorization dialog can drop the context in a SPA. |
| Access token is user specific, so APIs will have access to the real user as well as the permissions the user has in Liferay. | Client ID can be sniffed as part of the auth request. |
| Can refresh an access token without redirecting to Liferay for authorization. | Must persist the refresh token where the application can use it later, typically in local storage. |
| SPAs would need to leverage popup windows so main application can stay in the browser and retain context. | |
| Few library choices to help with the dialog and auth process. |
For my React SPA, I needed a popup window to do the Liferay authorization in. I ended up adapting https://github.com/Ramshackle-Jamathon/react-oauth-popup to handle the popup, it worked quite well (I added PKCE support covered in the next section).
While this is an effective system to handle authorization, it does expose the client ID and can potentially be used by another application to get an access token. If you are leaning towards implementing the Authorization Code Flow, I'd encourage you to take one step farther and implement the next flow.
PKCE Extended Authorization Code Flow
PKCE (pronounced "Pixie") is an acronym of Proof Key of Code Exchange. PCKE follows the same steps as the Authorization Code Flow but with the following changes:
For the /o/oauth2/authorize request, an additional value is passed in as the code_challenge parameter. This is a value that is passed through a one-way hash. The algorithm, shared below, will compute a value that is passed as the code_challenge value for verification in the next step.
For the /o/oauth2/token request for the access token, the pre-hashed value is sent as the code_verifier parameter.
The OAuth 2 provider will verify that the code_verifier code can be passed through the known hash to become the code_challenge provided in the authorize request.
Because of the hash value comparison, this flow helps to protect the client id and access token from misuse by bad actors.
Otherwise, the same steps from the Authorization Code Flow apply. The authorize request will require a login and an authorization dialog from Liferay. The received access token will come with a refresh token that can be used to get a new access token in the future without going through the authorization dialog again.
The PKCE flow is best for applications that may not be able to guarantee the security of the Client Secret for the application.
| Pros | Cons |
|---|---|
| No need to capture or know user credentials. | Redirects to Liferay for authorization dialog can drop the context in a SPA. |
| Access token is user specific, so APIs will have access to the real user as well as the permissions the user has in Liferay. | Must persist the refresh token where the application can use it later, typically in local storage. |
| Can refresh an access token without redirecting to Liferay for authorization. | SPAs would need to leverage popup windows so main application can stay in the browser and retain context. |
| Protects Client ID by requiring a code verifier and challenge. | Few library choices to help with the dialog and auth process. |
Creating the Code Verifier and Code Challenge Values
PKCE requires two pieces of data: a code verifier and a code challenge.
The code verifier value should be a random string using alphanumeric characters (plus the period, the dash, the underscore and the tilde characters) anywhere from 43 to 128 characters in length.
The code challenge is the base 64 encoded, SHA-256 hash of the code verifier value.
Code I used in my React application to create a code verifier value:
const S256_CHARS = [..."ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz-._~"];
this.codeVerifier = [...Array(43+(Math.random()*85|0))]
.map(i=>S256_CHARS[Math.random()*S256_CHARS.length|0]).join``;
The code I used to compute the code challenge value:
const hash = crypto.createHash('sha256').update(this.codeVerifier).digest();
const code_challenge = base64url.encode(hash);
For the code challenge value, I used the following NPM packages:
"base64url": "^3.0.1", "crypto": "^1.0.1",
Conclusion
So, where does that leave us?
I think you have two basic options to look at:
Client Credentials Flow - This one is great if you only need read-only access to Liferay APIs and don't care to audit what is being retrieved. It is a non-intrusive flow that any SPA can easily use to get an access token and retrieve records from Liferay.
PKCE Extended Authorization Code Flow - This one covers the cases where read-only don't apply, cases where you want to create, update or delete data in Liferay, cases where you want users to be represented by their own credentials and their own permissions applied to APIs they are invoking, and even cases where you want to track what the users are retrieving, this will be the flow for you. It is a tiny bit more on top of the Authorization Code Flow, but the extra verification is another layer meant to protect the application and the client ID.
For the other flows, Resource Owner exposes credentials and should immediately be disqualified. Authorization Code Flow is good, but with a few minor additional steps later allow you to implement PKCE, so why stop inches before the goal?
Hopefully this will make the job of choosing the right authorization flow easier.

