diff --git a/e2e/am-mock-api/src/app/response.webauthn.js b/e2e/am-mock-api/src/app/response.webauthn.js
new file mode 100644
index 0000000000..0d1b841360
--- /dev/null
+++ b/e2e/am-mock-api/src/app/response.webauthn.js
@@ -0,0 +1,171 @@
+/*
+ * @forgerock/javascript-sdk
+ *
+ * response.webauthn.js
+ *
+ * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved.
+ * This software may be modified and distributed under the terms
+ * of the MIT license. See the LICENSE file for details.
+ */
+
+/**
+ * WebAuthn registration initialization response
+ * Contains MetadataCallback for WebAuthn and HiddenValueCallback for credential
+ */
+export const webAuthnRegistrationInit = {
+ authId: 'webauthn-registration-init',
+ callbacks: [
+ {
+ type: 'MetadataCallback',
+ output: [
+ {
+ name: 'data',
+ value: {
+ _type: 'WebAuthn',
+ _action: 'webauthn_registration',
+ challenge: 'dGVzdC1jaGFsbGVuZ2UtZm9yLXdlYmF1dGhu',
+ relyingPartyId: 'localhost',
+ relyingPartyName: 'ForgeRock',
+ userId: 'dGVzdC11c2VyLWlk',
+ userName: 'testuser',
+ displayName: 'Test User',
+ timeout: 60000,
+ attestationPreference: 'none',
+ authenticatorAttachment: 'platform',
+ requireResidentKey: false,
+ userVerification: 'preferred',
+ pubKeyCredParams: [
+ { type: 'public-key', alg: -7 },
+ { type: 'public-key', alg: -257 },
+ ],
+ },
+ },
+ ],
+ },
+ {
+ type: 'HiddenValueCallback',
+ output: [
+ {
+ name: 'value',
+ value: '',
+ },
+ {
+ name: 'id',
+ value: 'webAuthnOutcome',
+ },
+ ],
+ input: [
+ {
+ name: 'IDToken2',
+ value: '',
+ },
+ ],
+ },
+ ],
+};
+
+/**
+ * Returns the recovery codes display response
+ * This simulates the step after WebAuthn registration where recovery codes are shown
+ */
+export function getRecoveryCodesDisplay() {
+ const recoveryCodes = [
+ 'ABC123DEF4',
+ 'GHI567JKL8',
+ 'MNO901PQR2',
+ 'STU345VWX6',
+ 'YZA789BCD0',
+ 'EFG123HIJ4',
+ 'KLM567NOP8',
+ 'QRS901TUV2',
+ 'WXY345ZAB6',
+ 'CDE789FGH0',
+ ];
+
+ // Build the recovery codes HTML similar to what AM generates
+ const codesHtml = recoveryCodes
+ .map((code) => `"
\\n" +\n "${code}\\n" +\n "
\\n" +`)
+ .join('\n ');
+
+ const scriptValue = `/*
+ * Copyright 2018 ForgeRock AS. All Rights Reserved
+ *
+ * Use of this code requires a commercial software license with ForgeRock AS.
+ * or with one of its affiliates. All use shall be exclusively subject
+ * to such license between the licensee and ForgeRock AS.
+ */
+
+var newLocation = document.getElementById("wrapper");
+var oldHtml = newLocation.getElementsByTagName("fieldset")[0].innerHTML;
+newLocation.getElementsByTagName("fieldset")[0].innerHTML = "\\n" +
+ "
\\n" +
+ "
Your Recovery Codes
\\n" +
+ " You must make a copy of these recovery codes. They cannot be displayed again.
\\n" +
+ " \\n" +
+ ${codesHtml}
+ "
\\n" +
+ "
Use one of these codes to authenticate if you lose your device, which has been named: New Security Key
\\n" +
+ "
\\n" +
+ "
" + oldHtml;
+document.body.appendChild(newLocation);
+`;
+
+ return {
+ authId: 'recovery-codes-display',
+ callbacks: [
+ {
+ type: 'TextOutputCallback',
+ output: [
+ {
+ name: 'message',
+ value: scriptValue,
+ },
+ {
+ name: 'messageType',
+ value: '4',
+ },
+ ],
+ },
+ {
+ type: 'ConfirmationCallback',
+ output: [
+ {
+ name: 'prompt',
+ value: '',
+ },
+ {
+ name: 'messageType',
+ value: 0,
+ },
+ {
+ name: 'options',
+ value: ['I have saved my recovery codes'],
+ },
+ {
+ name: 'optionType',
+ value: -1,
+ },
+ {
+ name: 'defaultOption',
+ value: 0,
+ },
+ ],
+ input: [
+ {
+ name: 'IDToken2',
+ value: 0,
+ },
+ ],
+ },
+ ],
+ };
+}
+
+/**
+ * Auth success response for WebAuthn flow
+ */
+export const authSuccess = {
+ tokenId: 'webauthn-session-token',
+ successUrl: '/console',
+ realm: '/',
+};
diff --git a/e2e/am-mock-api/src/app/responses.js b/e2e/am-mock-api/src/app/responses.js
index ccd1052feb..d7c9e7af41 100644
--- a/e2e/am-mock-api/src/app/responses.js
+++ b/e2e/am-mock-api/src/app/responses.js
@@ -1347,3 +1347,89 @@ export const recaptchaEnterpriseCallback = {
},
],
};
+
+export const qrCodeCallbacksResponse = {
+ authId: 'qrcode-journey-confirmation',
+ callbacks: [
+ {
+ type: 'TextOutputCallback',
+ output: [
+ {
+ name: 'message',
+ value:
+ 'Scan the QR code image below with the ForgeRock Authenticator app to register your device with your login.',
+ },
+ {
+ name: 'messageType',
+ value: '0',
+ },
+ ],
+ },
+ {
+ type: 'TextOutputCallback',
+ output: [
+ {
+ name: 'message',
+ value:
+ // eslint-disable-next-line quotes
+ "window.QRCodeReader.createCode({\n id: 'callback_0',\n text: 'otpauth\\x3A\\x2F\\x2Ftotp\\x2FForgeRock\\x3Ajlowery\\x3Fperiod\\x3D30\\x26b\\x3D032b75\\x26digits\\x3D6\\x26secret\\QITSTC234FRIU8DD987DW3VPICFY\\x3D\\x3D\\x3D\\x3D\\x3D\\x3D\\x26issuer\\x3DForgeRock',\n version: '20',\n code: 'L'\n});",
+ },
+ {
+ name: 'messageType',
+ value: '4',
+ },
+ ],
+ },
+ {
+ type: 'HiddenValueCallback',
+ output: [
+ {
+ name: 'value',
+ value:
+ 'otpauth://totp/ForgeRock:jlowery?secret=QITSTC234FRIU8DD987DW3VPICFY======&issuer=ForgeRock&period=30&digits=6&b=032b75',
+ },
+ {
+ name: 'id',
+ value: 'mfaDeviceRegistration',
+ },
+ ],
+ input: [
+ {
+ name: 'IDToken3',
+ value: 'mfaDeviceRegistration',
+ },
+ ],
+ },
+ {
+ type: 'ConfirmationCallback',
+ output: [
+ {
+ name: 'prompt',
+ value: '',
+ },
+ {
+ name: 'messageType',
+ value: 0,
+ },
+ {
+ name: 'options',
+ value: ['Next'],
+ },
+ {
+ name: 'optionType',
+ value: -1,
+ },
+ {
+ name: 'defaultOption',
+ value: 0,
+ },
+ ],
+ input: [
+ {
+ name: 'IDToken4',
+ value: 0,
+ },
+ ],
+ },
+ ],
+};
diff --git a/e2e/am-mock-api/src/app/routes.auth.js b/e2e/am-mock-api/src/app/routes.auth.js
index 3d13208ea8..158a61e763 100644
--- a/e2e/am-mock-api/src/app/routes.auth.js
+++ b/e2e/am-mock-api/src/app/routes.auth.js
@@ -48,8 +48,14 @@ import {
MetadataMarketPlaceInitialize,
MetadataMarketPlacePingOneEvaluation,
newPiWellKnown,
+ qrCodeCallbacksResponse,
} from './responses.js';
import initialRegResponse from './response.registration.js';
+import {
+ webAuthnRegistrationInit,
+ getRecoveryCodesDisplay,
+ authSuccess as webAuthnSuccess,
+} from './response.webauthn.js';
import wait from './wait.js';
console.log(`Your user password from 'env.config' file: ${USERS[0].pw}`);
@@ -86,13 +92,19 @@ export default function (app) {
) {
res.json(nameCallback);
} else if (req.query.authIndexValue === 'TEST_LoginPingProtect') {
- res.json(pingProtectInitialize);
+ res.json({ ...pingProtectInitialize, authId: 'protect-journey-init' });
} else if (req.query.authIndexValue === 'IDMSocialLogin') {
res.json(selectIdPCallback);
} else if (req.query.authIndexValue === 'TEST_MetadataMarketPlace') {
res.json(MetadataMarketPlaceInitialize);
} else if (req.query.authIndexValue === 'AMSocialLogin') {
res.json(idpChoiceCallback);
+ } else if (req.query.authIndexValue === 'TEST_WebAuthnWithRecoveryCodes') {
+ res.json(webAuthnRegistrationInit);
+ } else if (req.query.authIndexValue === 'QRCodeTest') {
+ res.json({ ...initialBasicLogin, authId: 'qrcode-journey-login' });
+ } else if (req.query.authIndexValue === 'DeviceProfileCallbackTest') {
+ res.json({ ...initialBasicLogin, authId: 'device-profile-journey-login' });
} else if (req.query.authIndexValue === 'RecaptchaEnterprise') {
res.json(initialBasicLogin);
} else {
@@ -166,13 +178,6 @@ export default function (app) {
}
}
return res.json(MetadataMarketPlacePingOneEvaluation);
- } else if (req.query.authIndexValue === 'QRCodeTest') {
- // If QR Code callbacks are being returned, return success
- if (req.body.callbacks.find((cb) => cb.type === 'HiddenValueCallback')) {
- return res.json(authSuccess);
- }
- // Client is returning callbacks from username password, so return QR Code callbacks
- res.json(otpQRCodeCallbacks);
} else if (req.query.authIndexValue === 'SAMLTestFailure') {
if (req.body.callbacks.find((cb) => cb.type === 'RedirectCallback')) {
if (
@@ -332,19 +337,24 @@ export default function (app) {
res.status(401).json(authFail);
}
}
- } else if (req.query.authIndexValue === 'TEST_LoginPingProtect') {
+ } else if (
+ req.query.authIndexValue === 'TEST_LoginPingProtect' ||
+ req.body.authId?.startsWith('protect-journey')
+ ) {
const protectInitCb = req.body.callbacks.find(
(cb) => cb.type === 'PingOneProtectInitializeCallback',
);
- const usernameCb = req.body.callbacks.find((cb) => cb.type === 'NameCallback');
+ const passwordCb = req.body.callbacks.find((cb) => cb.type === 'PasswordCallback');
const protectEvalCb = req.body.callbacks.find(
(cb) => cb.type === 'PingOneProtectEvaluationCallback',
);
+
if (protectInitCb) {
- res.json(initialBasicLogin);
- } else if (usernameCb && usernameCb.input[0].value) {
- res.json(pingProtectEvaluate);
- } else if (protectEvalCb && protectEvalCb.input[0].value) {
+ res.json({ ...initialBasicLogin, authId: 'protect-journey-login' });
+ } else if (passwordCb && passwordCb.input[0].value === USERS[0].pw) {
+ res.json({ ...pingProtectEvaluate, authId: 'protect-journey-eval' });
+ } else if (protectEvalCb) {
+ res.cookie('iPlanetDirectoryPro', 'protect-session-' + Date.now(), { domain: 'localhost' });
res.json(authSuccess);
} else {
res.status(401).json(authFail);
@@ -354,8 +364,14 @@ export default function (app) {
if (pwCb.input[0].value !== USERS[0].pw) {
res.status(401).json(authFail);
} else {
- if (req.query.authIndexValue === 'DeviceProfileCallbackTest') {
- res.json(requestDeviceProfile);
+ const authId = req.body.authId;
+ if (
+ req.query.authIndexValue === 'DeviceProfileCallbackTest' ||
+ authId === 'device-profile-journey-login'
+ ) {
+ res.json({ ...requestDeviceProfile, authId: 'device-profile-journey-collection' });
+ } else if (req.query.authIndexValue === 'QRCodeTest' || authId === 'qrcode-journey-login') {
+ res.json(qrCodeCallbacksResponse);
} else {
if (
req.body.stage === 'TransactionAuthorization' ||
@@ -385,35 +401,49 @@ export default function (app) {
const deviceCb = req.body.callbacks.find((cb) => cb.type === 'DeviceProfileCallback') || {};
const inputArr = deviceCb.input || [];
const input = inputArr[0] || {};
- const value = JSON.parse(input.value);
- const location = value.location || {};
+ const value = JSON.parse(input.value || '{}');
const metadata = value.metadata || {};
- // location is not allowed in some browser automation
- // const location = value.location || {};
- // We just need property existence to ensure profile is generated
- // We don't care about values since they are unique per browser
- if (
- location &&
- location.latitude &&
- location.longitude &&
+ const hasMetadata =
metadata.browser &&
metadata.browser.userAgent &&
metadata.platform &&
- metadata.platform.deviceName &&
- metadata.platform.fonts &&
- metadata.platform.fonts.length > 0 &&
- metadata.platform.timezone &&
- value.identifier &&
- value.identifier.length > 0
- ) {
+ metadata.platform.deviceName;
+
+ const hasIdentifier = value.identifier && value.identifier.length > 0;
+
+ if (hasMetadata && hasIdentifier) {
res.cookie('iPlanetDirectoryPro', 'abcd1234', { domain: 'localhost' });
res.json(authSuccess);
} else {
- // Just failing the auth for testing, but in reality,
- // an additional auth callback would be sent, like OTP
res.json(authFail);
}
+ } else if (
+ (req.query.authIndexValue === 'QRCodeTest' ||
+ req.body.authId === 'qrcode-journey-confirmation') &&
+ req.body.callbacks.find((cb) => cb.type === 'ConfirmationCallback')
+ ) {
+ res.cookie('iPlanetDirectoryPro', 'abcd1234', { domain: 'localhost' });
+ res.json(authSuccess);
+ } else if (
+ req.query.authIndexValue === 'TEST_WebAuthnWithRecoveryCodes' ||
+ req.body.authId?.startsWith('webauthn-registration') ||
+ req.body.authId?.startsWith('recovery-codes')
+ ) {
+ const metadataCb = req.body.callbacks.find((cb) => cb.type === 'MetadataCallback');
+ const hiddenCb = req.body.callbacks.find((cb) => cb.type === 'HiddenValueCallback');
+ const confirmationCb = req.body.callbacks.find((cb) => cb.type === 'ConfirmationCallback');
+
+ if (metadataCb && hiddenCb) {
+ res.json(getRecoveryCodesDisplay());
+ } else if (confirmationCb) {
+ res.cookie('iPlanetDirectoryPro', 'mock-webauthn-session-' + Date.now(), {
+ domain: 'localhost',
+ });
+ res.json(webAuthnSuccess);
+ } else {
+ res.status(401).json(authFail);
+ }
}
});
diff --git a/e2e/davinci-suites/src/utils/demo-user.ts b/e2e/davinci-suites/src/utils/demo-user.ts
index b9e9fd45c0..f29637e710 100644
--- a/e2e/davinci-suites/src/utils/demo-user.ts
+++ b/e2e/davinci-suites/src/utils/demo-user.ts
@@ -5,6 +5,6 @@
* of the MIT license. See the LICENSE file for details.
*/
export const username = 'demouser';
-export const password = 'U.QPDWEN47ZMyJhCDmhGLK*nr';
+export const password = 'yvk4uwq2edr@gxb7UWD';
export const phoneNumber1 = '888123456';
export const phoneNumber2 = '888123457';
diff --git a/e2e/journey-app/callback-map.ts b/e2e/journey-app/callback-map.ts
index 1f7fc328dd..a9b6e94831 100644
--- a/e2e/journey-app/callback-map.ts
+++ b/e2e/journey-app/callback-map.ts
@@ -60,11 +60,13 @@ import {
* @param journeyEl - The container element to append the component to
* @param callback - The callback instance
* @param idx - Index for generating unique IDs
+ * @param onSubmit - Optional callback to trigger form submission
*/
export function renderCallback(
journeyEl: HTMLDivElement,
callback: BaseCallback,
idx: number,
+ onSubmit?: () => void,
): void {
switch (callback.getType()) {
case 'BooleanAttributeInputCallback':
@@ -83,7 +85,7 @@ export function renderCallback(
confirmationComponent(journeyEl, callback as ConfirmationCallback, idx);
break;
case 'DeviceProfileCallback':
- deviceProfileComponent(journeyEl, callback as DeviceProfileCallback, idx);
+ deviceProfileComponent(journeyEl, callback as DeviceProfileCallback, idx, onSubmit);
break;
case 'HiddenValueCallback':
hiddenValueComponent(journeyEl, callback as HiddenValueCallback, idx);
@@ -101,10 +103,20 @@ export function renderCallback(
passwordComponent(journeyEl, callback as PasswordCallback, idx);
break;
case 'PingOneProtectEvaluationCallback':
- pingProtectEvaluationComponent(journeyEl, callback as PingOneProtectEvaluationCallback, idx);
+ pingProtectEvaluationComponent(
+ journeyEl,
+ callback as PingOneProtectEvaluationCallback,
+ idx,
+ onSubmit,
+ );
break;
case 'PingOneProtectInitializeCallback':
- pingProtectInitializeComponent(journeyEl, callback as PingOneProtectInitializeCallback, idx);
+ pingProtectInitializeComponent(
+ journeyEl,
+ callback as PingOneProtectInitializeCallback,
+ idx,
+ onSubmit,
+ );
break;
case 'PollingWaitCallback':
pollingWaitComponent(journeyEl, callback as PollingWaitCallback, idx);
@@ -149,9 +161,14 @@ export function renderCallback(
* Renders all callbacks in a step
* @param journeyEl - The container element to append components to
* @param callbacks - Array of callback instances
+ * @param onSubmit - Optional callback to trigger form submission
*/
-export function renderCallbacks(journeyEl: HTMLDivElement, callbacks: BaseCallback[]): void {
+export function renderCallbacks(
+ journeyEl: HTMLDivElement,
+ callbacks: BaseCallback[],
+ onSubmit?: () => void,
+): void {
callbacks.forEach((callback, idx) => {
- renderCallback(journeyEl, callback, idx);
+ renderCallback(journeyEl, callback, idx, onSubmit);
});
}
diff --git a/e2e/journey-app/components/device-profile.ts b/e2e/journey-app/components/device-profile.ts
index c0d62e9223..d452970ccc 100644
--- a/e2e/journey-app/components/device-profile.ts
+++ b/e2e/journey-app/components/device-profile.ts
@@ -4,12 +4,18 @@
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
+import { Device } from '@forgerock/journey-client/device';
import type { DeviceProfileCallback } from '@forgerock/journey-client/types';
+/**
+ * Device Profile Component
+ * Automatically collects device metadata and location data using the Device class
+ */
export default function deviceProfileComponent(
journeyEl: HTMLDivElement,
callback: DeviceProfileCallback,
idx: number,
+ onSubmit?: () => void,
) {
const collectorKey = callback?.payload?.input?.[0].name || `collector-${idx}`;
const message = document.createElement('p');
@@ -19,14 +25,36 @@ export default function deviceProfileComponent(
journeyEl?.appendChild(message);
- // Device profile callback typically runs automatically
- // The callback will collect device information in the background
- setTimeout(() => {
+ // Automatically trigger device profile collection
+ setTimeout(async () => {
try {
- // Device profile collection is typically handled automatically by the callback
- console.log('Device profile collection initiated');
+ const isLocationRequired = callback.isLocationRequired();
+ const isMetadataRequired = callback.isMetadataRequired();
+
+ console.log('Collecting device profile...', { isLocationRequired, isMetadataRequired });
+
+ // Create device instance and collect profile
+ const device = new Device();
+ const profile = await device.getProfile({
+ location: isLocationRequired,
+ metadata: isMetadataRequired,
+ });
+
+ console.log('Device profile collected successfully');
+
+ // Set the profile on the callback
+ callback.setProfile(profile);
+ message.innerText = 'Device profile collected successfully!';
+ message.style.color = 'green';
+
+ if (onSubmit) {
+ setTimeout(() => onSubmit(), 500);
+ }
} catch (error) {
console.error('Device profile collection failed:', error);
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
+ message.innerText = `Collection failed: ${errorMessage}`;
+ message.style.color = 'red';
}
}, 100);
}
diff --git a/e2e/journey-app/components/ping-protect-evaluation.ts b/e2e/journey-app/components/ping-protect-evaluation.ts
index 90ce809036..403e284f25 100644
--- a/e2e/journey-app/components/ping-protect-evaluation.ts
+++ b/e2e/journey-app/components/ping-protect-evaluation.ts
@@ -5,11 +5,17 @@
* of the MIT license. See the LICENSE file for details.
*/
import type { PingOneProtectEvaluationCallback } from '@forgerock/journey-client/types';
+import { getProtectInstance } from './ping-protect-initialize.js';
+/**
+ * PingOne Protect Evaluation Component
+ * Automatically collects device and behavioral signals using the Protect SDK
+ */
export default function pingProtectEvaluationComponent(
journeyEl: HTMLDivElement,
callback: PingOneProtectEvaluationCallback,
idx: number,
+ onSubmit?: () => void,
) {
const collectorKey = callback?.payload?.input?.[0].name || `collector-${idx}`;
const message = document.createElement('p');
@@ -19,5 +25,47 @@ export default function pingProtectEvaluationComponent(
journeyEl?.appendChild(message);
- // TODO: Implement PingOne Protect module evaluation here
+ // Automatically trigger Protect data collection
+ setTimeout(async () => {
+ try {
+ // Get the protect instance created during initialization
+ const protectInstance = getProtectInstance();
+
+ if (!protectInstance) {
+ throw new Error(
+ 'Protect instance not initialized. Initialize callback must be called first.',
+ );
+ }
+
+ console.log('Collecting Protect signals...');
+
+ // Collect device and behavioral data
+ const result = await protectInstance.getData();
+
+ // Check if result is an error object
+ if (typeof result !== 'string' && 'error' in result) {
+ console.error('Error collecting Protect data:', result.error);
+ callback.setClientError(result.error);
+ message.innerText = `Data collection failed: ${result.error}`;
+ message.style.color = 'red';
+ return;
+ }
+
+ // Set the collected data on the callback
+ console.log('Protect data collected successfully');
+ callback.setData(result);
+ message.innerText = 'Risk assessment completed successfully!';
+ message.style.color = 'green';
+
+ if (onSubmit) {
+ setTimeout(() => onSubmit(), 500);
+ }
+ } catch (error) {
+ console.error('Protect evaluation failed:', error);
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
+ callback.setClientError(errorMessage);
+ message.innerText = `Evaluation failed: ${errorMessage}`;
+ message.style.color = 'red';
+ }
+ }, 100);
}
diff --git a/e2e/journey-app/components/ping-protect-initialize.ts b/e2e/journey-app/components/ping-protect-initialize.ts
index c45215c6a4..6cbe21db28 100644
--- a/e2e/journey-app/components/ping-protect-initialize.ts
+++ b/e2e/journey-app/components/ping-protect-initialize.ts
@@ -4,12 +4,29 @@
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
+import { protect } from '@forgerock/protect';
import type { PingOneProtectInitializeCallback } from '@forgerock/journey-client/types';
+// Global storage for protect instance to be used by evaluation component
+let protectInstance: ReturnType | null = null;
+
+/**
+ * Gets the stored protect instance
+ * @returns The protect instance or null if not initialized
+ */
+export function getProtectInstance() {
+ return protectInstance;
+}
+
+/**
+ * PingOne Protect Initialize Component
+ * Automatically initializes the Protect SDK using configuration from the callback
+ */
export default function pingProtectInitializeComponent(
journeyEl: HTMLDivElement,
callback: PingOneProtectInitializeCallback,
idx: number,
+ onSubmit?: () => void,
) {
const collectorKey = callback?.payload?.input?.[0].name || `collector-${idx}`;
const message = document.createElement('p');
@@ -19,5 +36,54 @@ export default function pingProtectInitializeComponent(
journeyEl?.appendChild(message);
- // TODO: Implement PingOne Protect module initialization here
+ // Automatically trigger Protect initialization
+ setTimeout(async () => {
+ try {
+ // Get configuration from callback
+ const config = callback.getConfig();
+ console.log('Protect callback config:', config);
+
+ if (!config?.envId) {
+ const error = 'Missing envId in Protect configuration';
+ console.error(error);
+ callback.setClientError(error);
+ message.innerText = `Initialization failed: ${error}`;
+ message.style.color = 'red';
+ return;
+ }
+
+ console.log('Initializing Protect with envId:', config.envId);
+
+ // Create and store protect instance
+ protectInstance = protect({ envId: config.envId });
+ console.log('Protect instance created');
+
+ // Initialize the Protect SDK
+ console.log('Calling protect.start()...');
+ const result = await protectInstance.start();
+ console.log('protect.start() result:', result);
+
+ if (result?.error) {
+ console.error('Error initializing Protect:', result.error);
+ callback.setClientError(result.error);
+ message.innerText = `Initialization failed: ${result.error}`;
+ message.style.color = 'red';
+ return;
+ }
+
+ console.log('Protect initialized successfully - no errors');
+ message.innerText = 'PingOne Protect initialized successfully!';
+ message.style.color = 'green';
+
+ if (onSubmit) {
+ setTimeout(() => onSubmit(), 500);
+ }
+ } catch (error) {
+ console.error('Protect initialization failed:', error);
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
+ callback.setClientError(errorMessage);
+ message.innerText = `Initialization failed: ${errorMessage}`;
+ message.style.color = 'red';
+ }
+ }, 100);
}
diff --git a/e2e/journey-app/components/qr-code.ts b/e2e/journey-app/components/qr-code.ts
new file mode 100644
index 0000000000..1c1d5ff746
--- /dev/null
+++ b/e2e/journey-app/components/qr-code.ts
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2025 Ping Identity Corporation. All rights reserved.
+ *
+ * This software may be modified and distributed under the terms
+ * of the MIT license. See the LICENSE file for details.
+ */
+import { QRCode } from '@forgerock/journey-client/qr-code';
+import type { JourneyStep, ConfirmationCallback } from '@forgerock/journey-client/types';
+
+export function renderQRCodeStep(journeyEl: HTMLDivElement, step: JourneyStep): boolean {
+ if (!QRCode.isQRCodeStep(step)) {
+ return false;
+ }
+
+ const qrCodeData = QRCode.getQRCodeData(step);
+
+ console.log('QR Code step detected via QRCode module');
+ console.log('QR Code data:', JSON.stringify(qrCodeData));
+
+ const container = document.createElement('div');
+ container.id = 'qr-code-container';
+
+ const message = document.createElement('p');
+ message.id = 'qr-code-message';
+ message.innerText = qrCodeData.message || 'Scan the QR code below';
+ container.appendChild(message);
+
+ const uriDisplay = document.createElement('div');
+ uriDisplay.id = 'qr-code-uri';
+ uriDisplay.style.cssText = `
+ padding: 10px;
+ background-color: #f5f5f5;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ font-family: monospace;
+ font-size: 12px;
+ word-break: break-all;
+ margin: 10px 0;
+ `;
+ uriDisplay.innerText = qrCodeData.uri;
+ container.appendChild(uriDisplay);
+
+ const useType = document.createElement('p');
+ useType.id = 'qr-code-use-type';
+ useType.innerText = `Type: ${qrCodeData.use}`;
+ useType.style.color = '#666';
+ container.appendChild(useType);
+
+ const confirmationCallbacks =
+ step.getCallbacksOfType('ConfirmationCallback');
+
+ if (confirmationCallbacks.length > 0) {
+ const confirmCb = confirmationCallbacks[0];
+ const options = confirmCb.getOptions();
+
+ const optionsContainer = document.createElement('div');
+ optionsContainer.style.marginTop = '10px';
+
+ options.forEach((option, index) => {
+ const label = document.createElement('label');
+ label.style.display = 'block';
+ label.style.marginBottom = '5px';
+
+ const radio = document.createElement('input');
+ radio.type = 'radio';
+ radio.name = 'qr-confirmation';
+ radio.value = String(index);
+ radio.checked = index === confirmCb.getDefaultOption();
+ radio.addEventListener('change', () => {
+ confirmCb.setOptionIndex(index);
+ });
+
+ if (index === confirmCb.getDefaultOption()) {
+ confirmCb.setOptionIndex(index);
+ }
+
+ label.appendChild(radio);
+ label.appendChild(document.createTextNode(` ${option}`));
+ optionsContainer.appendChild(label);
+ });
+
+ container.appendChild(optionsContainer);
+ }
+
+ journeyEl.appendChild(container);
+
+ return true;
+}
diff --git a/e2e/journey-app/components/recovery-codes.ts b/e2e/journey-app/components/recovery-codes.ts
new file mode 100644
index 0000000000..c74ab3ee18
--- /dev/null
+++ b/e2e/journey-app/components/recovery-codes.ts
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2025 Ping Identity Corporation. All rights reserved.
+ *
+ * This software may be modified and distributed under the terms
+ * of the MIT license. See the LICENSE file for details.
+ */
+import { RecoveryCodes } from '@forgerock/journey-client/recovery-codes';
+import type { JourneyStep, ConfirmationCallback } from '@forgerock/journey-client/types';
+
+export function renderRecoveryCodesStep(journeyEl: HTMLDivElement, step: JourneyStep): boolean {
+ if (!RecoveryCodes.isDisplayStep(step)) {
+ return false;
+ }
+
+ const codes = RecoveryCodes.getCodes(step);
+ const deviceName = RecoveryCodes.getDeviceName(step);
+
+ console.log('Recovery Codes step detected via RecoveryCodes module');
+ console.log('Recovery codes:', JSON.stringify(codes));
+ console.log('Device name:', deviceName);
+
+ const container = document.createElement('div');
+ container.id = 'recovery-codes-container';
+
+ const header = document.createElement('h3');
+ header.id = 'recovery-codes-header';
+ header.innerText = 'Your Recovery Codes';
+ container.appendChild(header);
+
+ const instruction = document.createElement('p');
+ instruction.innerText =
+ 'You must make a copy of these recovery codes. They cannot be displayed again.';
+ instruction.style.color = '#666';
+ container.appendChild(instruction);
+
+ const codesContainer = document.createElement('div');
+ codesContainer.id = 'recovery-codes-list';
+ codesContainer.style.cssText = `
+ padding: 15px;
+ background-color: #f5f5f5;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ font-family: monospace;
+ font-size: 14px;
+ margin: 10px 0;
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 8px;
+ `;
+
+ codes.forEach((code, index) => {
+ const codeEl = document.createElement('div');
+ codeEl.className = 'recovery-code';
+ codeEl.setAttribute('data-code-index', String(index));
+ codeEl.innerText = code;
+ codeEl.style.cssText = `
+ padding: 5px 10px;
+ background-color: white;
+ border: 1px solid #ccc;
+ border-radius: 3px;
+ text-align: center;
+ `;
+ codesContainer.appendChild(codeEl);
+ });
+
+ container.appendChild(codesContainer);
+
+ if (deviceName) {
+ const deviceInfo = document.createElement('p');
+ deviceInfo.id = 'recovery-codes-device';
+ deviceInfo.innerText = `Device: ${deviceName}`;
+ deviceInfo.style.color = '#666';
+ container.appendChild(deviceInfo);
+ }
+
+ const confirmationCallbacks =
+ step.getCallbacksOfType('ConfirmationCallback');
+
+ if (confirmationCallbacks.length > 0) {
+ const confirmCb = confirmationCallbacks[0];
+ const options = confirmCb.getOptions();
+
+ const optionsContainer = document.createElement('div');
+ optionsContainer.style.marginTop = '15px';
+
+ options.forEach((option, index) => {
+ const label = document.createElement('label');
+ label.style.display = 'block';
+ label.style.marginBottom = '5px';
+
+ const radio = document.createElement('input');
+ radio.type = 'radio';
+ radio.name = 'recovery-confirmation';
+ radio.value = String(index);
+ radio.checked = index === confirmCb.getDefaultOption();
+ radio.addEventListener('change', () => {
+ confirmCb.setOptionIndex(index);
+ });
+
+ if (index === confirmCb.getDefaultOption()) {
+ confirmCb.setOptionIndex(index);
+ }
+
+ label.appendChild(radio);
+ label.appendChild(document.createTextNode(` ${option}`));
+ optionsContainer.appendChild(label);
+ });
+
+ container.appendChild(optionsContainer);
+ }
+
+ journeyEl.appendChild(container);
+
+ return true;
+}
diff --git a/e2e/journey-app/main.ts b/e2e/journey-app/main.ts
index f1eb3c6a99..d5868c1e2f 100644
--- a/e2e/journey-app/main.ts
+++ b/e2e/journey-app/main.ts
@@ -11,6 +11,8 @@ import { journey } from '@forgerock/journey-client';
import type { RequestMiddleware } from '@forgerock/journey-client/types';
import { renderCallbacks } from './callback-map.js';
+import { renderQRCodeStep } from './components/qr-code.js';
+import { renderRecoveryCodesStep } from './components/recovery-codes.js';
import { serverConfigs } from './server-configs.js';
const qs = window.location.search;
@@ -115,9 +117,15 @@ if (searchParams.get('middleware') === 'true') {
header.innerText = formName || '';
journeyEl.appendChild(header);
- const callbacks = step.callbacks;
+ const submitForm = () => formEl.requestSubmit();
- renderCallbacks(journeyEl, callbacks);
+ const stepRendered =
+ renderQRCodeStep(journeyEl, step) || renderRecoveryCodesStep(journeyEl, step);
+
+ if (!stepRendered) {
+ const callbacks = step.callbacks;
+ renderCallbacks(journeyEl, callbacks, submitForm);
+ }
const submitBtn = document.createElement('button');
submitBtn.type = 'submit';
diff --git a/e2e/journey-app/package.json b/e2e/journey-app/package.json
index 09ae71130a..7a83f030d8 100644
--- a/e2e/journey-app/package.json
+++ b/e2e/journey-app/package.json
@@ -16,6 +16,7 @@
"dependencies": {
"@forgerock/journey-client": "workspace:*",
"@forgerock/oidc-client": "workspace:*",
+ "@forgerock/protect": "workspace:*",
"@forgerock/sdk-logger": "workspace:*"
}
}
diff --git a/e2e/journey-app/tsconfig.app.json b/e2e/journey-app/tsconfig.app.json
index 3000be2f63..5d19cb58cd 100644
--- a/e2e/journey-app/tsconfig.app.json
+++ b/e2e/journey-app/tsconfig.app.json
@@ -19,6 +19,9 @@
{
"path": "../../packages/oidc-client/tsconfig.lib.json"
},
+ {
+ "path": "../../packages/protect/tsconfig.lib.json"
+ },
{
"path": "../../packages/journey-client/tsconfig.lib.json"
}
diff --git a/e2e/journey-app/tsconfig.json b/e2e/journey-app/tsconfig.json
index cc7b958851..a7028763ec 100644
--- a/e2e/journey-app/tsconfig.json
+++ b/e2e/journey-app/tsconfig.json
@@ -20,6 +20,9 @@
{
"path": "../../packages/oidc-client"
},
+ {
+ "path": "../../packages/protect"
+ },
{
"path": "../../packages/journey-client"
},
diff --git a/e2e/journey-suites/src/device-profile.test.ts b/e2e/journey-suites/src/device-profile.test.ts
index 65c8c348ee..e5770b3262 100644
--- a/e2e/journey-suites/src/device-profile.test.ts
+++ b/e2e/journey-suites/src/device-profile.test.ts
@@ -9,32 +9,78 @@ import { expect, test } from '@playwright/test';
import { asyncEvents } from './utils/async-events.js';
import { username, password } from './utils/demo-user.js';
-test.skip('Test happy paths on test page', async ({ page }) => {
+test('Test device profile collection journey flow', async ({ page }) => {
const { clickButton, navigate } = asyncEvents(page);
- await navigate('/?journey=TEST_DeviceProfile');
-
const messageArray: string[] = [];
+ let deviceProfileRequestBody: Record | null = null;
- // Listen for events on page
page.on('console', async (msg) => {
messageArray.push(msg.text());
return Promise.resolve(true);
});
- // Perform basic login
+ page.on('request', (request) => {
+ if (request.url().includes('/authenticate') && request.method() === 'POST') {
+ try {
+ const postData = request.postData();
+ if (!postData) return;
+
+ const body = JSON.parse(postData);
+ const deviceCallback = body.callbacks?.find(
+ (cb: { type: string }) => cb.type === 'DeviceProfileCallback',
+ );
+ if (!deviceCallback) return;
+
+ const profileInput = deviceCallback.input?.find(
+ (input: { name: string }) => input.name === 'IDToken1',
+ );
+ if (profileInput?.value) {
+ deviceProfileRequestBody = JSON.parse(profileInput.value);
+ }
+ } catch {
+ // Ignore parsing errors
+ }
+ }
+ });
+
+ await navigate('/?journey=DeviceProfileCallbackTest&clientId=basic');
+
+ await expect(page.getByLabel('User Name')).toBeVisible({ timeout: 10000 });
await page.getByLabel('User Name').fill(username);
await page.getByLabel('Password').fill(password);
await clickButton('Submit', '/authenticate');
- await expect(page.getByText('Collecting device profile')).toBeVisible();
+ await expect(page.getByText('Collecting device profile information...')).toBeVisible({
+ timeout: 10000,
+ });
+ await expect(page.getByText('Device profile collected successfully!')).toBeVisible({
+ timeout: 15000,
+ });
+
+ await expect(page.getByText('Complete')).toBeVisible({ timeout: 15000 });
+
+ expect(deviceProfileRequestBody).not.toBeNull();
+ expect(deviceProfileRequestBody).toHaveProperty('identifier');
+ expect(typeof deviceProfileRequestBody?.identifier).toBe('string');
+ expect((deviceProfileRequestBody?.identifier as string).length).toBeGreaterThan(0);
+
+ expect(deviceProfileRequestBody).toHaveProperty('metadata');
+ const metadata = deviceProfileRequestBody?.metadata as Record;
+ expect(metadata).toHaveProperty('hardware');
+ expect(metadata).toHaveProperty('browser');
+ expect(metadata).toHaveProperty('platform');
+
+ const platform = metadata.platform as Record;
+ expect(platform).toHaveProperty('deviceName');
+ expect(typeof platform.deviceName).toBe('string');
- await expect(page.getByText('Complete')).toBeVisible();
+ await clickButton('Logout', '/sessions');
- // Perform logout
- await clickButton('Logout', '/authenticate');
+ await expect(page.getByLabel('User Name')).toBeVisible({ timeout: 10000 });
- // Test assertions
- expect(messageArray.includes('Device profile collected successfully')).toBe(true);
- expect(messageArray.includes('Journey completed successfully')).toBe(true);
- expect(messageArray.includes('Logout successful')).toBe(true);
+ expect(messageArray.some((msg) => msg.includes('Device profile collected successfully'))).toBe(
+ true,
+ );
+ expect(messageArray.some((msg) => msg.includes('Journey completed successfully'))).toBe(true);
+ expect(messageArray.some((msg) => msg.includes('Logout successful'))).toBe(true);
});
diff --git a/e2e/journey-suites/src/protect.test.ts b/e2e/journey-suites/src/protect.test.ts
index bb7eb3ee11..09e37c4920 100644
--- a/e2e/journey-suites/src/protect.test.ts
+++ b/e2e/journey-suites/src/protect.test.ts
@@ -9,32 +9,75 @@ import { expect, test } from '@playwright/test';
import { asyncEvents } from './utils/async-events.js';
import { username, password } from './utils/demo-user.js';
-test.skip('Test happy paths on test page', async ({ page }) => {
- const { clickButton, navigate } = asyncEvents(page);
- await navigate('/?journey=TEST_Protect');
-
+test('Test PingOne Protect journey flow', async ({ page }) => {
+ const { clickButton } = asyncEvents(page);
const messageArray: string[] = [];
+ let protectSignalsData: string | null = null;
- // Listen for events on page
page.on('console', async (msg) => {
messageArray.push(msg.text());
return Promise.resolve(true);
});
- // Perform basic login
+ page.on('request', (request) => {
+ if (request.url().includes('/authenticate') && request.method() === 'POST') {
+ try {
+ const postData = request.postData();
+ if (postData) {
+ const body = JSON.parse(postData);
+ const callbacks = body.callbacks || [];
+ for (const callback of callbacks) {
+ if (callback.type === 'PingOneProtectEvaluationCallback') {
+ const inputs = callback.input || [];
+ for (const input of inputs) {
+ if (input.name === 'IDToken1signals' && input.value) {
+ protectSignalsData = input.value;
+ }
+ }
+ }
+ }
+ }
+ } catch {
+ // Ignore parsing errors
+ }
+ }
+ });
+
+ await page.goto('/?journey=TEST_LoginPingProtect&clientId=basic', { waitUntil: 'load' });
+
+ await expect(page.getByText('Initializing PingOne Protect...')).toBeVisible({ timeout: 10000 });
+ await expect(page.getByText('PingOne Protect initialized successfully!')).toBeVisible({
+ timeout: 15000,
+ });
+
+ await expect(page.getByLabel('User Name')).toBeVisible({ timeout: 15000 });
await page.getByLabel('User Name').fill(username);
await page.getByLabel('Password').fill(password);
await clickButton('Submit', '/authenticate');
- await expect(page.getByText('Collecting protect data')).toBeVisible();
+ await expect(page.getByText('Evaluating risk assessment...')).toBeVisible({ timeout: 10000 });
+ await expect(page.getByText('Risk assessment completed successfully!')).toBeVisible({
+ timeout: 15000,
+ });
+
+ // Wait for the evaluation callback to auto-submit and complete
+ await page.waitForResponse((response) => response.url().includes('/authenticate'));
+
+ await expect(page.getByText('Complete')).toBeVisible({ timeout: 15000 });
+
+ // Verify signals were captured from the request
+ expect(protectSignalsData).not.toBeNull();
+ expect(typeof protectSignalsData).toBe('string');
+ expect(protectSignalsData?.length).toBeGreaterThan(0);
- await expect(page.getByText('Complete')).toBeVisible();
+ await clickButton('Logout', '/sessions');
- // Perform logout
- await clickButton('Logout', '/authenticate');
+ await expect(page.getByText('Initializing PingOne Protect...')).toBeVisible({ timeout: 10000 });
- // Test assertions
- expect(messageArray.includes('Protect data collected successfully')).toBe(true);
- expect(messageArray.includes('Journey completed successfully')).toBe(true);
- expect(messageArray.includes('Logout successful')).toBe(true);
+ // Verify the protect SDK flow through console logs
+ expect(messageArray.some((msg) => msg.includes('Protect initialized successfully'))).toBe(true);
+ expect(messageArray.some((msg) => msg.includes('Protect data collected successfully'))).toBe(
+ true,
+ );
+ expect(messageArray.some((msg) => msg.includes('Logout successful'))).toBe(true);
});
diff --git a/e2e/journey-suites/src/qr-code.test.ts b/e2e/journey-suites/src/qr-code.test.ts
new file mode 100644
index 0000000000..b18ebd2d4d
--- /dev/null
+++ b/e2e/journey-suites/src/qr-code.test.ts
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2025 Ping Identity Corporation. All rights reserved.
+ *
+ * This software may be modified and distributed under the terms
+ * of the MIT license. See the LICENSE file for details.
+ */
+
+import { expect, test } from '@playwright/test';
+import { asyncEvents } from './utils/async-events.js';
+import { username, password } from './utils/demo-user.js';
+
+test('Test QR Code journey flow using QRCode module', async ({ page }) => {
+ const { clickButton, navigate } = asyncEvents(page);
+ const messageArray: string[] = [];
+
+ page.on('console', async (msg) => {
+ messageArray.push(msg.text());
+ return Promise.resolve(true);
+ });
+
+ await navigate('/?journey=QRCodeTest');
+
+ await page.getByLabel('User Name').fill(username);
+ await page.getByLabel('Password').fill(password);
+ await clickButton('Submit', '/authenticate');
+
+ await expect(page.locator('#qr-code-container')).toBeVisible({ timeout: 10000 });
+
+ await expect(page.locator('#qr-code-message')).toBeVisible();
+ const messageText = await page.locator('#qr-code-message').textContent();
+ expect(messageText).toContain('Scan the QR code');
+
+ await expect(page.locator('#qr-code-uri')).toBeVisible();
+ const uriText = await page.locator('#qr-code-uri').textContent();
+ expect(uriText).toContain('otpauth://');
+ expect(uriText).toContain('secret=');
+
+ await expect(page.locator('#qr-code-use-type')).toBeVisible();
+ const useTypeText = await page.locator('#qr-code-use-type').textContent();
+ expect(useTypeText).toContain('Type: otp');
+
+ await clickButton('Submit', '/authenticate');
+
+ await expect(page.getByText('Complete')).toBeVisible();
+
+ await clickButton('Logout', '/sessions');
+
+ await expect(page.getByLabel('User Name')).toBeVisible({ timeout: 10000 });
+
+ expect(messageArray.some((msg) => msg.includes('QR Code step detected via QRCode module'))).toBe(
+ true,
+ );
+ expect(messageArray.some((msg) => msg.includes('QR Code data:'))).toBe(true);
+ expect(messageArray.some((msg) => msg.includes('Journey completed successfully'))).toBe(true);
+ expect(messageArray.some((msg) => msg.includes('Logout successful'))).toBe(true);
+});
diff --git a/e2e/journey-suites/src/recovery-codes.test.ts b/e2e/journey-suites/src/recovery-codes.test.ts
new file mode 100644
index 0000000000..ce90a62607
--- /dev/null
+++ b/e2e/journey-suites/src/recovery-codes.test.ts
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2025 Ping Identity Corporation. All rights reserved.
+ *
+ * This software may be modified and distributed under the terms
+ * of the MIT license. See the LICENSE file for details.
+ */
+
+import { expect, test } from '@playwright/test';
+import { asyncEvents } from './utils/async-events.js';
+
+test.describe('Recovery Codes Journey', () => {
+ test('should display recovery codes using RecoveryCodes module and complete journey', async ({
+ page,
+ }) => {
+ const { clickButton, navigate } = asyncEvents(page);
+ const messageArray: string[] = [];
+
+ page.on('console', async (msg) => {
+ messageArray.push(msg.text());
+ return Promise.resolve(true);
+ });
+
+ await navigate('/?journey=TEST_WebAuthnWithRecoveryCodes');
+
+ await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible({ timeout: 10000 });
+ await clickButton('Submit', '/authenticate');
+
+ await expect(page.locator('#recovery-codes-container')).toBeVisible({ timeout: 10000 });
+
+ await expect(page.locator('#recovery-codes-header')).toBeVisible();
+ const headerText = await page.locator('#recovery-codes-header').textContent();
+ expect(headerText).toContain('Recovery Codes');
+
+ await expect(page.locator('#recovery-codes-list')).toBeVisible();
+
+ const codeElements = page.locator('.recovery-code');
+ const codeCount = await codeElements.count();
+ expect(codeCount).toBeGreaterThan(0);
+
+ const firstCode = await codeElements.first().textContent();
+ expect(firstCode).toBeTruthy();
+ expect(firstCode?.length).toBeGreaterThan(0);
+
+ await expect(page.getByText('I have saved my recovery codes')).toBeVisible();
+
+ await clickButton('Submit', '/authenticate');
+
+ await expect(page.getByText('Complete')).toBeVisible({ timeout: 10000 });
+
+ const sessionToken = await page.locator('#sessionToken').textContent();
+ expect(sessionToken).toBeTruthy();
+
+ await clickButton('Logout', '/sessions');
+
+ await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible({ timeout: 10000 });
+
+ expect(
+ messageArray.some((msg) =>
+ msg.includes('Recovery Codes step detected via RecoveryCodes module'),
+ ),
+ ).toBe(true);
+ expect(messageArray.some((msg) => msg.includes('Recovery codes:'))).toBe(true);
+ expect(messageArray.some((msg) => msg.includes('Journey completed successfully'))).toBe(true);
+ expect(messageArray.some((msg) => msg.includes('Logout successful'))).toBe(true);
+ });
+});
diff --git a/e2e/oidc-suites/src/utils/demo-users.ts b/e2e/oidc-suites/src/utils/demo-users.ts
index 351e055b06..aa7a0f61dd 100644
--- a/e2e/oidc-suites/src/utils/demo-users.ts
+++ b/e2e/oidc-suites/src/utils/demo-users.ts
@@ -10,4 +10,4 @@
export const pingAmUsername = 'sdkuser';
export const pingAmPassword = 'password';
export const pingOneUsername = 'demouser';
-export const pingOnePassword = 'U.QPDWEN47ZMyJhCDmhGLK*nr';
+export const pingOnePassword = 'yvk4uwq2edr@gxb7UWD';
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c94d201093..d8a2d2fa25 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4,6 +4,44 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
+catalogs:
+ default:
+ '@reduxjs/toolkit':
+ specifier: ^2.8.2
+ version: 2.10.1
+ immer:
+ specifier: ^10.1.1
+ version: 10.2.0
+ msw:
+ specifier: ^2.5.1
+ version: 2.12.1
+ effect:
+ '@effect/cli':
+ specifier: ^0.69.0
+ version: 0.69.2
+ '@effect/language-service':
+ specifier: ^0.35.2
+ version: 0.35.2
+ '@effect/opentelemetry':
+ specifier: ^0.56.1
+ version: 0.56.6
+ '@effect/platform':
+ specifier: ^0.90.0
+ version: 0.90.10
+ '@effect/platform-node':
+ specifier: 0.94.2
+ version: 0.94.2
+ '@effect/vitest':
+ specifier: ^0.23.9
+ version: 0.23.13
+ effect:
+ specifier: ^3.17.2
+ version: 3.19.3
+ vitest:
+ vitest:
+ specifier: ^3.0.4
+ version: 3.2.4
+
importers:
.:
@@ -278,6 +316,9 @@ importers:
'@forgerock/oidc-client':
specifier: workspace:*
version: link:../../packages/oidc-client
+ '@forgerock/protect':
+ specifier: workspace:*
+ version: link:../../packages/protect
'@forgerock/sdk-logger':
specifier: workspace:*
version: link:../../packages/sdk-effects/logger