- Node 18+
- NPM 8+
- Ensure CORS is enabled
- Create an OAuth client (Native/SPA client) with the following values:
- Client ID:
WebLoginWidgetClient
- Scopes:
openid email
- Sign in URLs:
http://localhost:5173/
(port number may vary) - Enable Implied Consent
- Ensure Token Endpoint Authentication Method is set to 'none'
- Grant Types contains
Authorization Code
- Client ID:
- Ensure you have the default "Login" journey/tree
- Create Vite app with
npm create vite
(more info can be found on Vite's docs) - Follow prompts, choosing React as your desired library
- Open the newly created directory in your IDE of choice
- With your terminal/bash, install the dependencies:
npm install
(or, simplynpm i
) - Run the app in developer mode:
npm run dev
- Copy the URL printed in the console to see the rendered app (usually
http://localhost:5173
)
Pro tip: using a different browser for development testing than the one you use to log into the ForgeRock platform. This is a good idea as admin user and test user sessions can collide causing odd authentication failures.
Install the Login Widget via your open terminal: npm install @forgerock/login-widget@beta
. The Widget is currently in beta and requires the use of the beta
tag.
Return back to the project in your IDE and look for the index.html
file. Since we will start with the Modal type component of the Widget, create a root element on which the Widget will mount by adding <div id="widget"></div>
towards the bottom of the <body>
element, but before the <script>
tag:
<!DOCTYPE html>
<html lang="en">
<head>
...
</head>
<body>
<div id="root"></div>
<div id="widget"></div>
<!-- new element -->
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
Before we get to the JavaScript, we have to do one more thing. Wrap the app's CSS in a @layer
. This helps control the CSS cascade and simply solves styling issues with very little effort. Open index.css
and App.css
and wrap them both with the below:
@layer app {
/* your app's css */
}
Open the main application file, usually called App.jsx
in your IDE. Import the Widget
class, the configuration
module, and the Widget's CSS.
import Widget, { configuration } from '@forgerock/login-widget';
import '@forgerock/login-widget/widget.css';
For now, just call the configuration
method within your App
function component and save off the return value to config
variable for later use. This internally prepares the Widget
for use.
function App() {
const [count, setCount] = useState(0);
// Initiate all the Widget modules
const config = configuration();
// ...
Before we can do much, we need to import the useEffect
from the React library. This is to control the execution of a few statements we will write next. After importing useEffect
let's write it into our component with an empty dependency array.
import React, { useEffect, useState } from 'react';
// ...
function App() {
// ...
useEffect(() => {}, []);
// ...
Note: The empty dependency array is to tell React this has no dependencies at this point and should only run once (there's some nuance here that I'm going to ignore).
Now that we have the useEffect
written, add the following inside of it:
- Instantiate the
Widget
class within thisuseEffect
- Pass an object as an argument with a target property containing the selected DOM element we created in a previous step
- Assign its return value to a
widget
variable - Return a function that calls
widget.$destroy()
Your useEffect
should now look like this:
useEffect(() => {
const widget = new Widget({ target: document.getElementById('widget') });
return () => {
widget.$destroy();
};
}, []);
Note: The reason for the returned function is for proper clean up when the React component unmounts. If it remounts, we won't get two widgets added to the DOM.
If you revisit your browser, you'll notice that the app doesn't look any different. This is because the Widget, by default, will be invisible at startup. To ensure it's working as expected, inspect the DOM in the browser developer tools. If you open the <div id="widget">
element in the DOM, you should see the Login Widget mounted within it.
An invisible Widget isn't all that useful, so our next task is to pull in the component
module to manage the component's events.
- Add the
component
module to our import from the@forgerock/login-widget
- Call the
component
function just under theconfiguration
function - Assign its return value to a
componentEvents
variable:
// ...
function App() {
// ...
const config = configuration();
const componentEvents = component();
// ...
Now that we have a reference to the component events observable, we can not only trigger an event (like open
), but we can also listen for events. Before we call the open
method, let's repurpose the existing button within the App
component.
- Within the button on click handler, change the
setCount
function tocomponentEvents.open
- Change the button text to read "Login":
<button
onClick={() => {
componentEvents.open();
}}>
Login
</button>
You can now revisit your test browser and click the "Login" button. The modal should open and have a "spinner" animating on repeat. This is expected. The Widget is currently waiting on information for it to render.
Now, click the button in the top-right to close the modal. The modal should be dismissed as expected.
Now that we have the modal mounted and functional, let's prepare to call the ForgeRock platform to get our login data.
Before the Widget can connect with the ForgeRock platform, we need to use that config
variable we created earlier. Call its set
method within the exiting useEffect
, and provide the configuration values for your ForgeRock server:
useEffect(() => {
config.set({
forgerock: {
serverConfig: {
baseUrl: 'https://example.forgeblocks.com/am',
timeout: 3000,
},
},
});
const widget = new Widget({ target: document.getElementById('widget')});
// ...
Now that we have the Widget configured for calling ForgeRock, let's import the journey
module to start our authentication flow:
import Widget, {
component,
configuration,
journey,
} from '@forgerock/login-widget';
Execute the journey
function and assign its returned value to a journeyEvents
variable. This can be done just underneath the other "event" variables:
// …
function App() {
// …
const config = configuration();
const componentEvents = component();
const journeyEvents = journey();
This new events observable will provide access to journey events. Within the Login button's on click handler add the start
method. Now, when we open the modal, we'll also call start
to request the user's first authentication step.
<button onClick={() => {
journeyEvents.start();
componentEvents.open();
}>
Login
</button>
You are now capable of authenticating a user. With an existing user in your ForgeRock system, log that user in and see what happens. If successful, you'll notice the modal will dismiss itself, but your app is not capturing anything from this action. Let's now capture this data.
There are multiple ways to capture the event of a successful login and accessing the user information. Let's start with using the journeyEvents
observable we created previously.
Within the existing useEffect
function:
- Call the
subscribe
method and assign its return value to a unsubscribe variable - Pass in a function that just logs the event being emitted
- Call the unsubscribe function within the
useEffect
's return function
// ...
useEffect(() => {
// ...
const widget = new Widget({ target: document.getElementById('widget') });
const journeyEventsUnsub = journeyEvents.subscribe((event) => {
console.log(event);
});
return () => {
widget.$destroy();
journeyEventsUnsub();
};
}, []);
Note: Unsubscribing from the observable is important to avoid memory leaks if the component mounts and unmounts frequently.
Revisit your app in the test browser, but remove all of the browser's cookies and Web Storage to ensure we have a fresh start. In Chromium browsers, you can find it under the "Application" tab of the browser tools. In Firefox and Safari, you can find it under the "Storage" tab.
Once you have deleted all the cookies and storage, refresh the browser and try to login your test user. Notice in the console that there's a lot of events being emitted. You are welcome to browse through the objects. Initially, you may not have much need for all this data, but over time, this information will likely become more valuable to you.
To narrow down all of this information, let's capture just one piece of the event: the user response after successfully logging in. To do that, let's add a simple conditional.
Add an if condition within the subscribe
callback function that tests for the existence of the user response.
const journeyEventsUnsub = journeyEvents.subscribe((event) => {
if (event.user.response) {
console.log(event.user.response);
}
});
With the above condition, we only log out the user information when it's truthy. This helps us narrow down only the information that's useful to us right now.
Remove all the cookies, Web Storage and refresh the page. Try logging in again, and you should see only one log of the user information when it's available.
Finally, let's repurpose the useState
hook that's already used in the component to save the user information.
- Change the zeroth index of the returned value from
count
touserInfo
- Change the first index of the returned value from
setCount
tosetUserInfo
- Change the default value passed into the
useState
from0
tonull
- Change the condition from just truthy to
userInfo !== event.user.response
- Replace the
console.log
with thesetUserInfo
function - Add the
userInfo
variable in the dependency array of theuseEffect
This is what the top part of your App
function component should look like thus far:
function App() {
const [userInfo, setUserInfo] = useState(null);
// Initiate all the Widget modules
const config = configuration();
const componentEvents = component();
const journeyEvents = journey();
useEffect(() => {
// Set the Widget's configuration
config.set({
forgerock: {
serverConfig: {
baseUrl: 'https://example.forgeblocks.com/am',
timeout: 3000,
}
}
});
// Instantiate the Widget and assign it to a variable
const widget = new Widget({ target: document.getElementById('widget-modal') });
// Subscribe to journey observable and assign unsubscribe function to variable
const journeyEventsUnsub = journeyEvents.subscribe((event) => {
if (userInfo !== event.user.response) {
setUserInfo(event.user.response);
}
});
// Return a function that destroys the Widget and unsubscribes from the journey observable
return () => {
widget.$destroy();
journeyEventsUnsub();
};
}, [userInfo]);
// ...
Note: The condition comparing userInfo
to event.user.response
just reduces the number of times the setUserInfo
is called as it will now only be called if what's set in the hook is different than what's emitted from the Widget.
Now that we have the user data set into our React component, let's print it out into the DOM.
- Replace the paragraph tag containing the text "Edit
src/App.jsx
and save to test HMR" with a<pre>
tag - Within the
<pre>
tag, write a pair of braces:{}
- Within these braces, use the
JSON.stringify
method to serialize theuserInfo
value
Your JSX should look like this:
<pre>{JSON.stringify(userInfo, null, ' ')}</pre>
Note: the null
and ' '
(literal space character) help format the JSON a bit.
After clearing the browser data, try logging the user in and observe the user info get rendered onto the page after success.
Our final action is to log the user out, clearing all the user-related cookies, storage and cache. To do this, we need one final module that we haven't imported yet: user
. Let's import that in.
import Widget, {
configuration,
component,
journey,
user,
} from '@forgerock/login-widget';
We're going to make our Login button a bit smarter, and have it as a Login button when the user is logged out, and a Logout button when the user is logged in.
- Wrap the button element with braces containing a ternary using the "falsiness" of the
userInfo
as the condition - When no
userInfo
exists (the user is logged out), render the Login button - Write a Logout button with an on click handler to run the
user.logout
function
The resulting JSX should look like this:
{
!userInfo ? (
<button
onClick={() => {
journeyEvents.start();
componentEvents.open();
}}>
Login
</button>
) : (
<button
onClick={() => {
user.logout();
}}>
Logout
</button>
)
}
Note, we don't have to worry about resetting the userInfo
with the setUserInfo
function because we are already "listening" to events emitted from the journeyEvents
subscription with the user
object nested within it.
If your app is already reacting to the presence of user info, it should be rendering the Logout button already. Click it and observe the application reacting. You should now be able to log a user in, and log a user out, all while the app is reacting to this change in state.
Repo reflecting completion of tutorial