Step 4. Implement authentication using the Ping SDK
Now that we have our environment and servers setup, let’s jump into the application! Within your IDE of choice, navigate to the reactjs-todo/client
directory. This directory is where you will spend the rest of your time.
First, open up the index.js
file, import the Config
module from the Ping SDK for JavaScript and call the setAsync()
method on this object:
/reactjs-todo/client/index.js
+ import { Config } from '@forgerock/javascript-sdk';
import React from 'react';
import { createRoot } from 'react-dom/client';
import Router from './router';
import { WELLKNOWN_URL, APP_URL, JOURNEY_LOGIN, WEB_OAUTH_CLIENT } from './constants';
import { AppContext, useGlobalStateMgmt } from './global-state';
import './styles/index.scss';
+ const urlParams = new URLSearchParams(window.location.search);
+ const journeyParam = urlParams.get('journey');
+
+ await Config.setAsync();
/**
* Initialize the React application
* This is an IIFE (Immediately Invoked Function Expression),
* so it calls itself.
*/
(async function initAndHydrate() {
@@ collapsed @@
The use of setAsync()
should always be the first SDK method called and is frequently done at the application’s top-level file. To configure the SDK to communicate with the journeys, OAuth clients, and realms of the appropriate server, pass a configuration object with the appropriate values.
The configuration object you will use in this instance will pull most of its values out of the .env
variables previously setup, which are mapped to constants within our constants.js
file.
Here’s an example config for an PingOne Advanced Identity Cloud tenant:
/reactjs-todo/client/index.js
import { Config } from '@forgerock/javascript-sdk';
import React from 'react';
import { createRoot } from 'react-dom/client';
import Router from './router';
import { WELLKNOWN_URL, APP_URL, JOURNEY_LOGIN, WEB_OAUTH_CLIENT } from './constants';
import { AppContext, useGlobalStateMgmt } from './global-state';
import './styles/index.scss';
const urlParams = new URLSearchParams(window.location.search);
const journeyParam = urlParams.get('journey');
await Config.setAsync(
+ {
+ clientId: WEB_OAUTH_CLIENT,
+ redirectUri: `${window.location.origin}/callback`,
+ scope: 'openid profile email address',
+ serverConfig: {
+ wellknown: WELLKNOWN_URL,
+ timeout: 3000,
+ },
+ tree: `${journeyParam || JOURNEY_LOGIN}`,
+ }
);
@@ collapsed @@
Go back to your browser and refresh the home page. There should be no change to what’s rendered, and no errors in the console. Now that the app is configured to your server, let’s wire up the simple login page.
Building the login page
First, let’s review how the application renders the home page:
index.js
> router.js
> views/home.js
> inline code + components (components/
)
For the login page, the same pattern applies except it has less code within the view file:
index.js
> router.js
> views/login.js
> components/journey/form.js
In the top-right of the home page, click Sign In to open the login page.
You should see a "loading" spinner and message that’s persistent since it doesn’t have the callbacks from your server that are needed to render the form. Obtaining these callbacks is the first task.

Since most of the action is taking place in reactjs-todo/client/components/journey/form.js
, open it and add the FRAuth
module from the Ping SDK for JavaScript:
reactjs-todo/client/components/journey/form.js
+ import { FRAuth } from '@forgerock/javascript-sdk';
import React from 'react';
import Loading from '../utilities/loading';
@@ collapsed @@
FRAuth
is the first object used as it provides the necessary methods for authenticating a user by using an authentication journey or tree. Use the start()
method of FRAuth
as it returns data we need for rendering the form.
You will need to add new imports. Add useContext
and useState
from the React package.
You’ll use the useState()
method for managing the data received from the server, and the useEffect
is needed due to the FRAuth.start()
method resulting in a network request.
reactjs-todo/client/components/journey/form.js
import { FRAuth } from '@forgerock/javascript-sdk';
- import React from 'react';
+ import React, { useEffect, useState } from 'react';
import Loading from '../utilities/loading';
export default function Form() {
+ const [step, setStep] = useState(null);
+ useEffect(() => {
+ async function getStep() {
+ try {
+ const initialStep = await FRAuth.start();
+ console.log(initialStep);
+ setStep(initialStep);
+ } catch (err) {
+ console.error(`Error: request for initial step; ${err}`);
+ }
+ }
+ getStep();
+ }, []);
return <Loading message="Checking your session ..." />;
}
We are passing an empty array as the second argument into To learn more, refer to What an Effect with empty dependencies means in the React Developer Documentation. |
This code prints the response to starting the journey to the debug console in the browser. This response contains the first step of the journey and its callbacks
. These callbacks are the instructions for what needs to be rendered to the user to collect their input.

callbacks
returned from the server.Below is a summary of what you’ll do to get the form to react to the new callback data:
-
Import the needed form-input components
-
Create a function to map received
callbacks
to the appropriate component -
Use the components to render the appropriate UI for each callback in the response from the server
First, import the Alert
, AppContext
, Password
, Text
, and Unknown
components.
reactjs-todo/client/components/journey/form.js
import { FRAuth } from '@forgerock/javascript-sdk';
import React, { useEffect, useState } from 'react';
+ import Alert from './alert';
+ import Password from './password';
+ import Text from './text';
+ import Unknown from './unknown';
import Loading from '../utilities/loading';
@@ collapsed @@
Next, within the Form
function body, create the function that maps these imported components to their appropriate callback.
reactjs-todo/client/components/journey/form.js
@@ collapsed @@
export default function Form() {
const [step, setStep] = useState(null);
@@ collapsed @@
+ function mapCallbacksToComponents(cb, idx) {
+ const name = cb?.payload?.input?.[0].name;
+ switch (cb.getType()) {
+ case 'NameCallback':
+ return <Text callback={cb} inputName={name} key='username' />;
+ case 'PasswordCallback':
+ return <Password callback={cb} inputName={name} key='password' />;
+ default:
+ // If current callback is not supported, render a warning message
+ return <Unknown callback={cb} key={`unknown-${idx}`} />;
+ }
+ }
return <Loading message="Checking your session ..." />;
}
Finally, check for the presence of the step.callbacks
, and if they exist, map over them with the function from above. Replace the single return of <Loading message="Checking your session ..." />
with the following:
reactjs-todo/client/components/journey/form.js
@@ collapsed @@
+ if (!step) {
return <Loading message='Checking your session ...' />;
+ } else if (step.callbacks?.length) {
+ return (
+ <form className='cstm_form'>
+ {step.callbacks.map(mapCallbacksToComponents)}
+ <button className='btn btn-primary w-100' type='submit'>
+ Sign In
+ </button>
+ </form>
+ );
+ } else {
+ return <Alert message={step.payload.message} />;
* }
}
Refresh the page, and you should now have a dynamic form that reacts to the callbacks returned from our initial call to ForgeRock.

Handling the login form submission
Since a form that can’t submit anything isn’t very useful, we’ll now handle the submission of the user input values to ForgeRock. First, let’s edit the current form element, <form className="cstm_form">
, and add an onSubmit
handler with a simple, inline function.
reactjs-todo/client/components/journey/form.js
@@ collapsed @@
- <form className='cstm_form'>
+ <form
+ className="cstm_form"
+ onSubmit={(event) => {
+ event.preventDefault();
+ async function getStep() {
+ try {
+ const nextStep = await FRAuth.next(step);
+ console.log(nextStep);
+ setStep(nextStep);
+ } catch (err) {
+ console.error(`Error: form submission; ${err}`);
+ }
+ }
+ getStep();
+ }}
+ >
Refresh the login page and use the test user to login. You will get a mostly blank login page if the user’s credentials are valid and the journey completes. You can verify this by going to the Network panel within the developer tools and inspect the last /authenticate
request. It should have a tokenId
and successUrl
property.

You may ask, "How did the user’s input values get added to the step
object?" Let’s take a look at the component for rendering the username input. Open up the Text
component: components/journey/text.js
. Notice how special methods are being used on the callback object. These are provided as convenience methods by the SDK for getting and setting data.
reactjs-todo/client/components/journey/text.js
@@ collapsed @@
export default function Text({ callback, inputName }) {
const [state] = useContext(AppContext);
const existingValue = callback.getInputValue();
const textInputLabel = callback.getPrompt();
function setValue(event) {
callback.setInputValue(event.target.value);
}
return (
<div className={`cstm_form-floating form-floating mb-3`}>
<input
className={`cstm_form-control form-control ${validationClass} bg-transparent ${state.theme.textClass} ${state.theme.borderClass}`}
defaultValue={existingValue}
id={inputName}
name={inputName}
onChange={setValue}
placeholder={textInputLabel}
/>
<label htmlFor={inputName}>{textInputLabel}</label>
</div>
);
}
The two important items to focus on are the callback.getInputValue()
and the callback.setInputValue()
. The getInputValue
retrieves any existing value that may be provided by ForgeRock, and the setInputValue
sets the user’s input on the callback while they are typing (i.e. onChange
). Since the callback
is passed from the Form
to the components by "reference" (not by "value"), any mutation of the callback
object within the Text
(or Password
) component is also contained within the step
object in Form
.
You may think, "That’s not very idiomatic React! Shared, mutable state is bad." And, yes, you are correct, but we are taking advantage of this to keep everything simple (and this guide from being too long), so I hope you can excuse the pattern. |
Each callback type has its own collection of methods for getting and setting data in addition to a base set of generic callback methods. These methods are added to the callback prototype by the SDK automatically. For more information about these callback methods, see our API documentation, or the source code in GitHub, for more details.
Now that the form is rendering and submitting, add conditions to the Form
component for handling the success and error response from ForgeRock. This condition handles the success result of the authentication journey.
reactjs-todo/client/components/journey/form.js
@@ collapsed @@
if (!step) {
return <Loading message='Checking your session ...' />;
+ } else if (step.type === 'LoginSuccess') {
+ return <Alert message="Success! You're logged in." type='success' />;
} else if (step.callbacks?.length) {
@@ collapsed @@
Once you handle the success and error condition, return to the browser and remove all cookies created from any previous logins. Refresh the page and login with your test user created in the Setup section above. You should see a "Success!" alert message. Congratulations, you are now able to authenticate users!
