Liferay OAuth 2.0 Authorization Flows

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.

Blogs

Hi David,

 

first of all thanks for this blog post, very helpful to compare the different approaches Liferay offers as OAuth2 authentication flows. I've a question about the "Resource Owner password credentials". Instead of passing user credentials on the URL (as GET request) Liferay can handle the request as a POST  'application/x-www-form-urlencoded' request? In this case, the credentials are not passed as clear text in the URL. What's your opinion about using this flow in this manner?

 

The Resource Owner auth flow should be a POST, not a GET.  I don't know if the values can be passed in the body using x-www-form-encoded but I would bet that it is supported...

If user/pass can be passed as POST parameters, it's not clear to me  why the Cons "Exposes username and password in cleartext".

Because the request, itself, is not secure. It still holds the username and password in cleartext and is therefore a risk. Consider a MITM attack where your request gets routed through a bad guy's infrastructure, you'd still be passing the username and password in cleartext in a form the bad guy can consume.

Yes, they can be passed in the body. So this 

curl -d 'grant_type=password&username=...&password=...&client_id=...&client_secret=...'  https://SERVER/o/oauth2/token

 

is perfectly fine and if used with HTTPS credentials are  rather secure.  One thing to keep in mind is that some of the OAuth 2 application templates we ask you to choose from, prevents you from having value for "client_secret" field. In such case you should not have "client_secret" field in your request rather than sending an empty one. See:   https://issues.liferay.com/browse/LPS-102662  

At the end of the day, though, the password is neither encrypted nor encoded, so it is still passed in cleartext. So it remains a risk either from the client side (rogue JS library, etc) or a MITM attack or ...

It is the exactly same risk you are facing when login in on any HTTPS site ;)

If you are saying the risk is that the client (JS app) has to keep the credentials in plain text - that is true but it has nothing to do with the authorization flow. The application may instead securely ask for them on demand or use own means to encrypt them or ...   That is whole different discussion. 

The point I'm making is that the authorization flows itself can be perfectly secured.

Hi David, I'm trying to configure on Liferay DXP 7.1 an OAuth2 "User Agent Application". If I don't enable the PKCE, every time I call the the  authorize URL (https://[hostname]/o/oauth2/authorize?response_type=code&client_id=[client ID]) and authenticate with right user/password Liferay redirected  to [your callback URI]?error=unauthorized_client

If I enable the PKCE Liferay shows the authorization portlet successfully.

 

It's an expected behaviour or is it a bug? (if the OAuth configuration UI  for an User Agent Application let the user disable the PKCE it seems a bug to me). What do you think? (I have tested  it on 7.2 and is the same).

I don't think you should hit unauthorized user if the creds are right. My first few attempts I didn't have all of the necessary settings (for CORS, etc) and my calls were failing, even though I thought I had everything right.

 

Are you using form encoding and posts and everything correctly? It was only after I was doing everything right that my calls started to go through...

 

Are you coming to Devcon? We could look at it together if you are...

Hi David, one more question. I've configured an OAuth2 application as "User Agent Application"  but refresh token is not available.  Is it a Liferay choice or an OAuth2 recommendation to avoid refresh tokens usage with SPA?

 

By the way, if I'm not wrong refresh token in Liferay never expires. Do you think can be added a feature request for refresh token rotation? (a feature that invalidates a refresh token and issues a new one whenever it is used to refresh an access token. Rif. https://auth0.com/blog/oauth2-implicit-grant-and-spa/)

 

Thanks,

Denis

Not all of the flows support a refresh token; I don't think it is intended as a recommendation for SPAs, I just think it is defined as part of the authorization flow.

 

Refresh tokens do expire, but I think the default expiration time is really long. If you check the response you get back from oauth you'll find a field that holds the refresh token ttl info. The timeout is not fixed, it can be configured btw.

Hi David,

 

I've seen that Liferay has already implemented the Refresh Token Rotation (https://issues.liferay.com/browse/OAUTH2-227).  It's not enabled by default - cool thing!!! The duration of refresh token as you mentioned (is not in the the response in accordingly with RFC) can be configured in OAuth2 System Settings. The default in Liferay 7.2  is 7 days (604800).

 

About "User Agent Application" I suppose it's a Liferay configuration wizard (OAuth2 reccomends now to use authorization code flow with an empty client_secret). I don't understand why refresh tokens are not optional. WDYT? Can you check it with some Liferay Engineer why?

 

(I'm sorry, unfortunately I'll not at DEVCON. Without anabling PCKE I can't get authz step working...).

 

Hi David Thanks for your blog But It can't be useful for a interface like React Native that is a mobile application framework. Opening login form inside a popup window in a mobile application or even opening it inside a inApp-Browser inside a mobile app it's not a principled and satisfying solution. There is no solid solution for this kind of authentication?

Hi, I have come here trying to figure out how to use Liferay to do OAuth2 authentication (have something working with Auth0 and want to use Liferay instead... but getting stuck... another storey).

Anyway, the reason I am posting is I (in my humble and not too experianced opinion - espically in Liferay) think that some of the statements made above are incorrect...

In mutliple places it is mentioned that having the username and password on the URL for an https connetion exposes them to whoever wants to look in cleartext.

I believe this is not correct. In https, the URL after the domain name is enctypted in transit.

This is what I always believed and can be verfied by multiple sources, e.g. https://stackoverflow.com/questions/499591/are-https-urls-encrypted

Just thought it worth noting that ruling out some of the authrotsation flows because of this might not be correct (if that is ineed what you are saying).

Also, thanks for your articles... I find them generally helpful :-)