ForgeRock Login Widget
Table of Contents
- Overview
- Requirements
- Quick Start
- Observables Pattern
- Complete Widget API
- Supported Callbacks
- Disclaimer
- License
Overview
The Login Widget is an all-inclusive UI component for handling login, registration, and related user flows in any modern JavaScript app. It works with React, Vue, Angular, Svelte, or vanilla JavaScript — it does not currently support Node.js or server-side rendering (SSR).
The widget uses Journey Client for journey execution, and OIDC Client for OAuth/OIDC tokens and user info. It adds a UI rendering layer on top of these SDKs to eliminate the need to develop and maintain UI components for complex authentication flows. Although this rendering layer is developed with Svelte and Tailwind, both are "compiled away" and have no runtime dependencies. The resulting widget is library- and framework-agnostic.
The widget can be rendered in two form factors:
- Modal (default): Renders form elements inside a modal dialog that opens and closes. Mounted outside your app's controlled DOM.
- Inline: Just the form elements with no container. Mounted inside your app's controlled DOM.
Both form factors provide the same authentication, OAuth/OIDC, and user features. Start with Modal — it provides the quickest path to adding login/registration with minimal disruption to your existing codebase.
Requirements
- An ECMAScript module or CommonJS enabled client-side JavaScript app
- A modern, fully-supported browser: Chrome, Firefox, Safari, or Chromium Edge
Not supported: Internet Explorer, Legacy Edge, WebView, Electron, and other modified browser-like environments.
Quick Start
Install
npm install @forgerock/login-widget
Add CSS
Import into your JavaScript:
import '@forgerock/login-widget/widget.css';
Import into your CSS:
@import '@forgerock/login-widget/widget.css';
Or link in HTML (copy the CSS file from the npm module into your static files):
<link rel="stylesheet" href="/path/to/file/widget.css" />
Controlling the CSS Cascade
Though not required, using @layer ensures the browser applies CSS in the intended order regardless of import order. Read more about @layer.
-
Wrap your CSS in a layer:
@layer app { /* Your app's CSS */ } -
Declare layer order in your HTML before any CSS loads:
<style type="text/css"> @layer app; @layer 'fr-widget.base'; @layer 'fr-widget.utilities'; @layer 'fr-widget.components'; @layer 'fr-widget.variants'; </style>Widget CSS is fully namespaced — it will not overwrite your app's CSS unless you use the same selector naming convention.
Mount the Widget
Add a dedicated element to your HTML file as a direct child of <body>, separate from your main app root:
<body>
<div id="root"></div>
<div id="widget-root"></div>
</body>
Tip: Do not mount the modal form factor on a dynamic element inside your app's Virtual DOM. Hard-code it in your static HTML file.
Import, configure, and instantiate:
import Widget, { configure, journey } from '@forgerock/login-widget';
// 1. Configure — async; awaiting it ensures both clients are ready before use
await configure({
// REQUIRED — the well-known URL, shared by the journey and OIDC clients
serverConfig: {
wellknown:
'https://your-tenant.forgeblocks.com/am/oauth2/alpha/.well-known/openid-configuration',
},
// REQUIRED if you use OAuth/OIDC tokens, user info, or logout
oidcClient: {
clientId: 'YourOauthClient',
redirectUri: `${window.location.origin}/callback`,
scope: 'openid profile email', // OPTIONAL — defaults to 'openid'
},
});
// 2. Instantiate
const widgetRootEl = document.getElementById('widget-root');
new Widget({ target: widgetRootEl });
// 3. Start a journey
const journeyEvents = journey();
journeyEvents.start();
Tip:
configure()is async — alwaysawaitit at the top level of your application (index.jsorapp.js) before callingjourney().start()or any other Widget API. This ensures both the OIDC and journey clients are fully constructed before any fetch is attempted.
Observables Pattern
Most Widget APIs are asynchronous and use an event-centric observable pattern. The widget uses Svelte's simplified, standard observable implementation called a "store" — these are embedded in the widget and are not a dependency your app needs to manage.
Read more about the Svelte store contract.
Subscribe to Events
import { user } from '@forgerock/login-widget';
const userInfoEvents = user.info();
userInfoEvents.subscribe((event) => {
if (event.loading) {
console.log('User info is being requested from server');
} else if (event.successful) {
console.log('User info request was successful');
console.log(event.response);
} else if (event.error) {
console.error('User info request failed');
console.error(event.error.message);
}
});
Unsubscribe
Observables are not like Promises — they don't resolve and get cleaned up. You must unsubscribe when no longer needed, especially in components that are created and destroyed frequently:
const unsubscribe = userInfoEvents.subscribe((event) => console.log(event));
// Later, when no longer needed:
unsubscribe();
Note: If subscribing at a top-level component that persists over the lifetime of your app, unsubscribing is not needed.
Get Current Values
To get the current value stored within the Widget without subscribing to future events, call subscribe and immediately call the returned unsubscribe function:
let userInfo;
userInfoEvents.subscribe((event) => (userInfo = event.response))();
Request Fresh Values
Call action methods like get to request new values from the server. Any active subscribe callbacks will receive the resulting events:
userInfoEvents.get();
Use Promises
All Widget APIs that involve network calls also support Promises:
// async/await
const userInfo = await userInfoEvents.get();
// Promise chain
userInfoEvents
.get()
.then((data) => console.log(data))
.catch((err) => console.error(err));
Complete Widget API
Widget
import Widget from '@forgerock/login-widget';
// Instantiate
const widget = new Widget({
target: widgetRootEl, // REQUIRED; a real DOM element
props: {
type: 'modal', // OPTIONAL; "modal" (default) or "inline"
},
});
// Remove widget from DOM and destroy listeners
widget.$destroy();
Note: Call
$destroy()if instantiating the Widget in a component that is frequently created and destroyed. We recommend instantiating higher in your application tree, in a component created once and preserved.
Configuration
import { configure } from '@forgerock/login-widget';
// configure() is async — await it before calling any other Widget API
await configure({
// REQUIRED — the well-known URL, shared by the journey and OIDC clients
serverConfig: {
wellknown:
'https://your-tenant.forgeblocks.com/am/oauth2/realms/root/realms/alpha/.well-known/openid-configuration',
},
// REQUIRED if you use OAuth/OIDC tokens, user info, or logout
oidcClient: {
clientId: 'WebOAuthClient',
redirectUri: `${window.location.origin}/callback`,
scope: 'openid profile email',
},
// OPTIONAL — logger for both clients; `level` gates verbosity and `custom`
// redirects SDK log output to your own sink. See the full example below.
logger: { level: 'warn' },
// OPTIONAL — request middleware for both clients; see the full example below
middleware: [],
// OPTIONAL — token storage config; see Storage section below
storage: { type: 'sessionStorage', name: 'tokens' },
// OPTIONAL — see dedicated sections below
content: {},
links: {},
style: {},
});
Migration note (2.0.0): The
forgerockconfig object has been replaced byoidcClient. Endpoint discovery is now driven byserverConfig.wellknown, shared by the journey and OIDC clients —baseUrl,realmPath,timeout, andsupportare no longer used.clientIdandredirectUriare required when configuringoidcClient;scopedefaults to'openid'.tokenStorehas moved to a top-levelstorageoption.
Logger
The top-level logger option is forwarded to both the journey and OIDC clients.
| Property | Type | Default | Description |
|---|---|---|---|
level | 'none' | 'error' | 'warn' | 'info' | 'debug' | 'error' | Gates SDK log verbosity. 'none' silences all SDK logs. |
custom | { error, warn, info, debug } | — | Sink for SDK log output. When set, the SDK calls your methods instead of the console. |
await configure({
serverConfig: {
wellknown:
'https://your-tenant.forgeblocks.com/am/oauth2/alpha/.well-known/openid-configuration',
},
logger: {
level: 'debug',
// OPTIONAL — route SDK logs to your own sink instead of the console.
custom: {
error: (...args) => myLogger.error(...args),
warn: (...args) => myLogger.warn(...args),
info: (...args) => myLogger.info(...args),
debug: (...args) => myLogger.debug(...args),
},
},
oidcClient: {
clientId: 'WebOAuthClient',
redirectUri: `${window.location.origin}/callback`,
scope: 'openid profile email',
},
});
OIDC Client Options
All properties are nested inside oidcClient.
| Property | Type | Default | Description |
|---|---|---|---|
clientId | string | — | Required. OAuth 2.0 client ID. |
redirectUri | string | — | Required. URI AM redirects to after authorization. |
scope | string | 'openid' | OAuth 2.0 scopes. |
oauthThreshold | number | 30000 | Milliseconds before expiry to trigger background renewal. |
par | boolean | auto | Use Pushed Authorization Requests. When omitted, the SDK auto-detects from the authorization server's require_pushed_authorization_requests metadata. Setting false while the server requires PAR is an error. |
loginHint | string | — | Pre-fills the login identifier; bridged onto silent token renewal. |
acrValues | string | — | Requested ACR values; bridged onto silent token renewal. |
query | Record<string, string> | — | Extra authorize query params; bridged onto silent token renewal. |
Example with all optional OIDC options:
await configure({
serverConfig: {
wellknown:
'https://your-tenant.forgeblocks.com/am/oauth2/alpha/.well-known/openid-configuration',
},
logger: { level: 'debug' },
middleware: [
(req, action, next) => {
console.log('[middleware]', action.type, req.url);
next();
},
],
storage: { type: 'sessionStorage', name: 'tokens', prefix: 'myapp' },
oidcClient: {
clientId: 'WebOAuthClient',
redirectUri: `${window.location.origin}/callback`,
scope: 'openid profile email',
oauthThreshold: 60000,
par: true,
loginHint: '[email protected]',
acrValues: 'urn:acr:2fa',
query: { ui_locales: 'en-US' },
},
});
Storage
The top-level storage option configures where the OIDC client persists tokens. It mirrors
the SDK's StorageConfig union — type selects a browser store or a custom sink.
| Property | Type | Default | Description |
|---|---|---|---|
type | 'localStorage' | 'sessionStorage' | 'custom' | 'localStorage' | Storage backend. 'custom' requires a custom sink. |
name | string | — | Required. Storage key name. |
prefix | string | 'pic' | Key prefix for storage entries. |
custom | { get, set, remove } | — | Required when type: 'custom'. Async functions for your own store. |
// Browser store
storage: { type: 'sessionStorage', name: 'tokens', prefix: 'myapp' },
// Custom store
storage: {
type: 'custom',
name: 'tokens',
custom: {
get: async (key) => myStore.read(key),
set: async (key, value) => myStore.write(key, value),
remove: async (key) => myStore.delete(key),
},
},
Journey
import { journey } from '@forgerock/login-widget';
const journeyEvents = journey({
oauth: true, // OPTIONAL; default true; use OAuth flow for tokens
user: true, // OPTIONAL; default true; fetch user info from /userinfo
});
// Start a journey
journeyEvents.start({
journey: 'Login', // OPTIONAL; journey name (omit to let AM choose the default)
resumeUrl: window.location.href, // OPTIONAL; URL for resuming a suspended journey
});
// Change to a different journey
journeyEvents.change({
journey: 'Registration',
});
// Listen for events
journeyEvents.subscribe((event) => {
if (event.journey.successful) {
console.log('Authentication succeeded', event);
}
});
Journey event schema:
{
journey: {
completed: false, // boolean
error: null, // null or { code, message, step }
loading: false, // boolean
step: null, // null or step object from ForgeRock AM
successful: false, // boolean
response: null, // null or success response from AM
},
oauth: {
completed: false, // boolean
error: null, // null or { code, message }
loading: false, // boolean
successful: false, // boolean
response: null, // null or OAuth/OIDC tokens
},
user: {
completed: false, // boolean
error: null, // null or { code, message }
loading: false, // boolean
successful: false, // boolean
response: null, // null or user info (driven by OAuth scope)
},
}
Component
The component API controls the widget's lifecycle. For the modal form factor, it provides open and close methods.
import { component } from '@forgerock/login-widget';
const componentEvents = component();
// Open/close the modal
componentEvents.open();
componentEvents.close();
// Listen for lifecycle events
componentEvents.subscribe((event) => {
if (event.mounted) console.log('Widget is mounted');
if (event.open === false) console.log('Modal closed:', event.reason);
});
Component event schema:
{
error: null, // null or { code, message, step }
mounted: false, // boolean
open: null, // boolean or null (null for inline type)
reason: null, // "user" | "auto" | "external"
type: null, // "modal" | "inline"
}
Close reasons:
| Reason | Description |
|---|---|
"user" | User closed the dialog via UI |
"auto" | Modal closed after successful authentication |
"external" | Application called componentEvents.close() |
User
import { user } from '@forgerock/login-widget';
// User info
const userEvents = user.info();
userEvents.subscribe((event) => console.log(event));
userEvents.get(); // Fetch fresh user info from server
// User tokens
const tokenEvents = user.tokens();
tokenEvents.subscribe((event) => console.log(event));
tokenEvents.get(); // Fetch fresh tokens from server
// Logout
user.logout(); // Clears user data and emits events to subscribers
User info / token event schema:
{
completed: false, // boolean
error: null, // null or { code, message }
loading: false, // boolean
successful: false, // boolean
response: null, // object from /userinfo or /access_token endpoint
}
Calling protected resources
Removed in 2.0.0: The
requestexport (an alias to the legacyHttpClient.request) has been removed.@forgerock/oidc-clientdoes not provide an HTTP client. Get the access token fromuser.tokens()and callfetchdirectly, adding theAuthorizationheader yourself:
import { user } from '@forgerock/login-widget';
const tokenEvents = user.tokens();
const { response: tokens } = await tokenEvents.get();
const response = await fetch('https://protected.resource.com', {
method: 'GET',
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
},
});
Note: The legacy
requestautomatically refreshed tokens on a 401 and parsed Identity Gateway policy advice. Those behaviors are not provided by the new SDK and must be implemented by the consumer if needed.
Styling Configuration
Configure the widget's visual appearance:
await configure({
style: {
checksAndRadios: 'animated', // OPTIONAL; 'animated' or 'standard'
labels: 'floating', // OPTIONAL; 'floating' or 'stacked'
logo: {
dark: 'https://example.com/img/white-logo.png',
light: 'https://example.com/img/black-logo.png', // REQUIRED if logo provided
height: 300,
width: 400,
},
sections: {
// OPTIONAL; modal only
header: false, // Display a header section with logo
},
stage: {
icon: true, // OPTIONAL; display generic stage icons
},
callbacks: {
// OPTIONAL; per-callback style rules, keyed by AM callback type name
// — see "Scripted Text Output"; rules are plain string dictionaries
TextOutputCallback: [{ type: '4', display: 'hidden' }],
},
},
});
Note: The
sections.headerproperty only applies to the modal form factor. Thelogoproperty applies to all form factors.
Logo CSS custom properties
The logo configuration is applied as root-level CSS custom properties on the widget root
element, re-applied after every theme pass. These are the fallback slots; an IDM page-node
theme sets the primary --logo-light / --logo-dark slots and takes precedence over them.
| Property | CSS custom property | Notes |
|---|---|---|
light | --fr-logo-light-fallback | Consumed via var(--logo-light, var(--fr-logo-light-fallback)) |
dark | --fr-logo-dark-fallback | Consumed via var(--logo-dark, var(--fr-logo-dark-fallback)) |
height | --fr-logo-height | Falls back to 72px when unset; the dialog header logo stretches to the header height instead |
width | --fr-logo-width | Falls back to 200px on stages, 100% in the dialog; the dialog header logo stretches to the header height when unset |
Valid light / dark values are https:// URLs, data:image/ URIs, or root-relative paths
(leading /). Invalid values are dropped by config validation.
Links Configuration
Set the URL for your Terms & Conditions page (used by TermsAndConditionsCallback):
await configure({
links: {
termsAndConditions: 'https://example.com/terms',
},
});
Content Configuration
Override the widget's default content with custom text. For the full schema, see the en-US locale file.
await configure({
content: {
// Custom content that overrides Widget defaults
},
});
CAPTCHA Configuration
AM does not signal invisible mode in the callback payload for either ReCaptchaCallback or ReCaptchaEnterpriseCallback. Use the captcha option to configure invisible rendering:
await configure({
captcha: {
mode: 'invisible', // 'visible' (default) | 'invisible'
},
});
Script loading: The widget automatically injects the required CAPTCHA script at mount time — no manual <script> tag is needed. If the provider API (window.grecaptcha / window.hcaptcha) is already present on the page when the widget mounts, injection is skipped.
Scripted Text Output
A journey can return client-side JavaScript as a TextOutputCallback with messageType 4 — for example a tracking or device-fingerprinting snippet. The widget does not execute these scripts, so by default it prints the script source to the screen as text.
Hide them with a per-callback rule list under callbacks:
await configure({
style: {
callbacks: {
// omit for default: prints the script source
TextOutputCallback: [{ type: '4', display: 'hidden' }],
},
},
});
Each rule is a plain string dictionary passed through to that callback's renderer. type selects a TextOutputCallback message type and display: 'hidden' suppresses it. Informational (0), warning (1), and error (2) messages render as usual. The scripts are still not executed either way — the option only controls whether their source is displayed.
Supported Callbacks
The widget supports the following ForgeRock callbacks:
- Page node
- Username, Password
- WebAuthn (registration and login)
- Push authentication
- One-time password verification
- Social login (Apple, Facebook, Google)
- Email suspend ("magic links")
- Device profile
- reCAPTCHA v2 (visible + invisible), reCAPTCHA Enterprise, hCaptcha (visible + invisible)
- QR codes
- Ping Protect
Disclaimer
This code is provided by ForgeRock on an "as is" basis, without warranty of any kind, to the fullest extent permitted by law. ForgeRock does not represent or warrant or make any guarantee regarding the use of this code or the accuracy, timeliness or completeness of any data or information relating to this code, and ForgeRock hereby disclaims all warranties whether express, or implied or statutory, including without limitation the implied warranties of merchantability, fitness for a particular purpose, and any warranty of non-infringement. ForgeRock shall not have any liability arising out of or related to any use, implementation or configuration of this code, including but not limited to use for any commercial purpose. Any action or suit relating to the use of the code may be brought only in the courts of a jurisdiction wherein ForgeRock resides or in which ForgeRock conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions.
License
This project is licensed under the MIT License — see the LICENSE file for details.
© Copyright 2022-2026 Ping Identity Corporation. All Rights Reserved.