oo

Using OAuth2 to Authorize Users

You can create applications that access Liferay’s headless REST APIs using the OAuth 2.0 authorization protocol. The provided sample React app demonstrates three different OAuth2 token-based authentication flows: authorization code flow, client credentials flow, and password flow. For more details on the OAuth2 Administration panel see Creating an OAuth2 Application

Set Up Liferay DXP

Start a new Liferay DXP instance by running

docker run -it -m 8g -p 8080:8080 liferay/dxp:2024.q1.1

Sign in to Liferay at http://localhost:8080 using the email address test@liferay.com and the password test. When prompted, change the password to learn.

  1. Open the Global Menu (Global Menu) and navigate to Control PanelSecurityOAuth 2 Administration.

  2. Click Add (Add Icon) to create a new OAuth2 application.

  3. Give the application a name (e.g., foo). Set the Website URL as http://localhost:3000 and the Callback URI as http://localhost:3000/grant-type-authorization-code. Click Save.

    Fill out the form to the create a new OAuth2 application.

  4. Copy the Client ID and Client Secret to your clipboard. To get the Client Secret click Edit. Copy the value from the pop-up window.

    These values are needed later in the sample React app.

  5. Click the Scopes tab at the top of the page. Scroll down, click LIFERAY.HEADLESS.ADMIN.USER, then check the box for read data on your behalf.

    Enable read privileges for LIFERAY.HEADLESS.ADMIN.USER.

    Click Save. Your OAuth2 Application now has read privileges for the Admin User API category.

  6. Next, open the Global Menu (Global Menu), click the Control Panel tab, and go to System SettingsSecurity Tools.

  7. Go to the Portal Cross-Origin Resource Sharing (CORS) tab and click Default Portal CORS Configuration.

  8. Add a URL Pattern with the value /o/headless-admin-user/* and click Save. This enables CORS for the headless-admin-user category of APIs.

Deploy the Sample React App

  1. Download and unzip the OAuth2 React App.

    curl https://resources.learn.liferay.com/dxp/latest/en/headless-delivery/using-oauth2/liferay-c2b6.zip -O
    
    unzip liferay-c2b6.zip
    
    cd liferay-c2b6
    
  2. Verify that node and yarn are installed. If not, run the setup script and follow the prompts:

    ./setup_tutorial.sh
    
  3. Navigate into the app’s root directory and start the React server.

    cd c2b6-custom-element
    
    yarn install && yarn start
    

Authorization Code Flow

The authorization code flow requires users to log in with their credentials and approve authorization before permission is granted to the app. This additional step is avoided with the other flows.

  1. Open the React app running at http://localhost:3000, then click Authorization Code Flow at the top of the page.

  2. Enter http://localhost:8080/o/oauth2/authorize for the Liferay authorize URL. Paste the client ID from your clipboard. Click Authorize.

  3. If you’re not already logged in, you are redirected to the Liferay login page before being sent to the authorization page. Enter your username and password (e.g. test@liferay.com:learn) and click Sign In. On the authorization page, click Authorize. If you are already logged in, you are sent directly to the authorization page.

    Click Authorizez to authorize the application.

    You are redirected back to the React app. Note the authorization code that is used to make the next API call appears.

  4. Enter http://localhost:8080/o/oauth2/token for the Liferay token URL. Paste the client ID and client secret from your clipboard. Click Get Token. Note the authorization token that can be used to make future REST API calls appears.

  5. Enter http://localhost:8080/o/headless-admin-user/v1.0/user-accounts for the Liferay get user URL. Click Get Data. The React app makes a REST API call to Liferay using token-based authentication and returns a list of Liferay users.

    A list of Liferay users appears.

Client Credentials Flow

The client credentials flow is used typically for server to server interaction and does not involve any users.

  1. Open the React app running at http://localhost:3000, then click Client Credentials Flow at the top of the page.

  2. Enter http://localhost:8080/o/oauth2/token for the Liferay token URL. Paste the client ID and client secret from your clipboard. Click Get Token. Note the authorization token that can be used to make future REST API calls appears.

  3. Enter http://localhost:8080/o/headless-admin-user/v1.0/user-accounts for the Liferay get user URL. Click Get Data. The React app makes a REST API call to Liferay using token-based authentication and returns a list of Liferay users.

Password Flow

In the password flow authorization, the React app passes the username and password directly in the request.

Warning

In a password flow, the username and password are directly exposed to the application. Thus the user must trust the application. Passing the username and password in the API request also carries risk. Using password flow is not recommended.

  1. Open the React app running at http://localhost:3000, then click Password Flow at the top of the page.

  2. Enter http://localhost:8080/o/oauth2/token for the Liferay token URL.

  3. Paste the client ID and client secret from your clipboard.

  4. Enter your username and password (e.g. test@liferay.com:learn).

  5. Click Get Token. Note the authorization token that can be used to make future REST API calls appears.

  6. Enter http://localhost:8080/o/headless-admin-user/v1.0/user-accounts for the Liferay get user URL. Click Get Data. The React app makes a REST API call to Liferay using token-based authentication and returns a list of Liferay users.

Examine the Code

The React app’s components folder defines the UI elements and logic for handling the events fired by clicking buttons. For example, the Authorize.js file redirects to the authorization page, Token.js fetches the access token, and Users.js fetches the list of users. Each authorization flow uses some or all of these components.

The API request to get an authorization token and the GET request is defined in the Requests.js file in the utils folder.

src
├── components
│   ├── Authorize.js
│   ├── Token.js
│   ├── Users.js
├── routes
│   ├── grant-type-authorization-code
│   |   ├── AuthorizationCode.js
│   ├── grant-type-client-credentials
│   |   ├── ClientCredentials.js
│   ├── grant-type-password
│   |   ├── Password.js
├── utils
│   ├── Requests.js
├── App.js
└── index.js

Authorization Grant Type

The AuthorizationCode.js file of the grant-type-authorization-code flow uses all three components (Authorize.js, Token.js, and Users.js)

<h1>Authorization Code Flow</h1>

Callback URI: http://localhost:3000/grant-type-authorization-code (or wherever the React app is running)

<br />

Scope: Headless Admin User (e.g. Liferay.Headless.Admin.User.everything.read)

<br />

CORS: Configure Portal CORS URL Pattern (e.g. /o/headless-admin-user/*)

<Authorize />

<Token
	grantType='authorization_code'
	handleToken={handleToken}
/>

<Users token={token} />

In the code, the Authorize.js component handles the first step. A request for authorization is made when the Authorize button is clicked.

function Authorize() {
	const [authUrl, setAuthUrl] = React.useState('');
	const [clientId, setClientId] = React.useState('');

	function handleAuthorize(event) {
		event.preventDefault();

		try {
			window.location.replace(
				authUrl + '?response_type=code&client_id=' + clientId
			);
		}
		catch (error) {
			throw new Error(error);
		}
	}

	const urlSearchParams = new URLSearchParams(window.location.search);

	var code = urlSearchParams.get('code');

	return (
		<div>
			<h2>Authorize</h2>

			<input
				onChange={(event) => setAuthUrl(event.target.value)}
				placeholder="Liferay Authorize URL"
				style={{width: 500}}
				type="text"
				value={authUrl}
			/>

			(e.g. http://localhost:8080/o/oauth2/authorize)

			<br />

			<input
				onChange={(event) => setClientId(event.target.value)}
				placeholder="Client ID"
				style={{width: 500}}
				type="text"
				value={clientId}
			/>

			<br />

			<form onSubmit={handleAuthorize}>
				<button type='onSubmit'>Authorize</button>
			</form>

			{code && (
				<div>
					<br />

					Authorization Code:

					<br />

					{code}
				</div>
			)}
		</div>
	);
}

Along with the redirect, a one-time authorization code is passed back to the app in the URL (e.g. http://localhost:3000/grant-type-authorization-code?code={code}).

The Token.js component handles the next step. Clicking Get Token calls the getAuthToken function in the Requests.js file.

import {getAuthToken} from '../utils/Requests';

function Token({handleToken, grantType}) {
	const [clientId, setClientId] = React.useState('');
	const [clientSecret, setClientSecret] = React.useState('');
	const [password, setPassword] = React.useState('');
	const [token, setToken] = React.useState('');
	const [tokenUrl, setTokenUrl] = React.useState('');
	const [username, setUsername] = React.useState('');

	async function handleGetToken() {
		const token = await getAuthToken({clientId, clientSecret, grantType, password, tokenUrl, username});

		handleToken(token);

		setToken(token);
	}

	return (
		<div className='Token'>
			<h2>Get Token</h2>

			<input
				onChange={(event) => setTokenUrl(event.target.value)}
				placeholder="Liferay Token URL"
				style={{width: 500}}
				type="text"
				value={tokenUrl}
			/>

			(e.g. http://localhost:8080/o/oauth2/token)

			<br />

			<input
				onChange={(event) => setClientId(event.target.value)}
				placeholder="Client ID"
				style={{width: 500}}
				type="text"
				value={clientId}
			/>

			<br />

			<input
				onChange={(event) => setClientSecret(event.target.value)}
				placeholder="Client Secret"
				style={{width: 500}}
				type="text"
				value={clientSecret}
			/>

			<br />

			{grantType === 'password' && (
				<div>
					<input
						onChange={(client) => setUsername(client.target.value)}
						placeholder="User Name"
						style={{width: 500}}
						type="text"
						value={username}
					/>

					<br />

					<input
						onChange={(client) =>
							setPassword(client.target.value)
						}
						placeholder="User Password"
						style={{width: 500}}
						type="text"
						value={password}
					/>

					<br />
				</div>
			)}

			<button onClick={handleGetToken}>Get Token</button>

			{token && (
				<div>
					<br />

					Authorization Token:

					<br />

					{token.access_token}
				</div>
			)}
		</div>
	);
}

In the Token.js component, the parameters client_id, client_secret, code, grant_type, and redirect_uri are sent in this API request. If the parameters are valid, Liferay returns a JSON response containing the access token.

Example response:

{
  "access_token": "2fda85abec524112dae612d35e9f9abd71650d364dee47c645b7574c6bffe91",
  "token_type": "Bearer",
  "expires_in": 600,
  "scope": "Liferay.Headless.Admin.User.everything.read"
}

The Users.js component parses the response for the access_token.

Finally, clicking Get Data calls the getUsers function in the Requests.js file.

Client Credentials Grant Type

The ClientCredentials.js file of the grant-type-client-credentials flow uses two components (i.e. Token.js and Users.js).

function ClientCredentials() {
	const [token, setToken] = React.useState({});

	function handleToken(token) {
		setToken(token);
	}

	return (
		<div>
			<h1>Client Credentials Code Flow</h1>

			Scope: Headless Admin User (e.g. Liferay.Headless.Admin.User.everything.read)

			<br />

			CORS: Configure Portal CORS URL Pattern (e.g. /o/headless-admin-user/*)

			<Token
				grantType='client_credentials'
				handleToken={handleToken}
			/>

			<Users token={token} />
		</div>
	);
}

In the Token.js component, the parameters client_id, client_secret, and grant_type are sent in the API request. If the parameters are valid, Liferay returns a JSON response containing the access token.

The Users.js component parses the response for the access_token.

Finally, clicking Get Data calls the getUsers function in the Requests.js file.

Password Grant Type

The Password.js file of the grant-type-password flow uses two components (i.e. Token.js and Users.js).

function Password() {
	const [token, setToken] = React.useState({});

	function handleToken(token) {
		setToken(token);
	}

	return (
		<div>
			<h1>Password Flow</h1>

			Scope: Headless Admin User (e.g. Liferay.Headless.Admin.User.everything.read)

			<br />

			CORS: Configure Portal CORS URL Pattern (e.g. /o/headless-admin-user/*)

			<Token
				grantType='password'
				handleToken={handleToken}
			/>

			<Users token={token} />
		</div>
	);
}

In the Token.js component, the parameters client_id, client_secret, grant_type, password, and username are sent as parameters in the API request. If the parameters are valid, the Liferay server returns a JSON response containing the access token.

The Users.js component parses the response for the access_token.

Finally, clicking Get Data calls the getUsers function in the Requests.js file.

Capability: