diff --git a/.gitignore b/.gitignore index 9830032f4d3..51377a04fbc 100644 --- a/.gitignore +++ b/.gitignore @@ -88,10 +88,12 @@ test-output .cursor/rules/nx-rules.mdc .github/instructions/nx.instructions.md +# Claude .claude/worktrees .claude/settings.local.json .claude/skills .claude/CLAUDE.md +tasks/ .opensource diff --git a/e2e/am-mock-api/src/app/responses.js b/e2e/am-mock-api/src/app/responses.js index 6127ea6e3de..6f3df3ad2b7 100644 --- a/e2e/am-mock-api/src/app/responses.js +++ b/e2e/am-mock-api/src/app/responses.js @@ -3,7 +3,7 @@ * * responses.js * - * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * Copyright (c) 2020 - 2026 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. */ @@ -60,7 +60,6 @@ export const createTxnStepUpUrl = (url) => { redirectUrl.searchParams.set('authIndexType', 'composite_advice'); redirectUrl.searchParams.set( 'authIndexValue', - // eslint-disable-next-line max-len '%3CAdvices%3E%3CAttributeValuePair%3E%3CAttribute%20name%3D%22TransactionConditionAdvice%22/%3E%3CValue%3E39dfdd15-59a3-473c-a7fc-ecda3bbc3bc8%3C/Value%3E%3C/AttributeValuePair%3E%3C/Advices%3E', ); @@ -78,7 +77,6 @@ export const createTreeStepUpUrl = (url) => { redirectUrl.searchParams.set('authIndexType', 'composite_advice'); redirectUrl.searchParams.set( 'authIndexValue', - // eslint-disable-next-line max-len '%3CAdvices%3E%3CAttributeValuePair%3E%3CAttribute%20name=%22AuthenticateToServiceConditionAdvice%22/%3E%3CValue%3E/sdk:ConfirmPassword%3C/Value%3E%3C/AttributeValuePair%3E%3C/Advices%3E', ); @@ -140,7 +138,6 @@ export const emailSuspend = { { name: 'message', value: - // eslint-disable-next-line max-len 'An email has been sent to the address you entered. Click the link in that email to proceed.', }, { name: 'messageType', value: '0' }, @@ -367,6 +364,37 @@ export const pingProtectInitialize = { ], }; +export const pingProtectSignalsInitializationOptions = { + authId: 'foo', + callbacks: [ + { + type: 'PingOneProtectInitializeCallback', + output: [ + { + name: 'signalsInitializationOptions', + value: { + behavioralDataCollection: 'true', + consoleLogEnabled: 'true', + disableTags: 'false', + disableHub: 'false', + lazyMetadata: 'false', + deviceAttributesToIgnore: 'userAgent', + universalDeviceIdentification: 'false', + agentIdentification: 'false', + htmlGeoLocation: 'false', + }, + }, + ], + input: [ + { + name: 'IDToken1clientError', + value: '', + }, + ], + }, + ], +}; + export const choiceCallback = { authId: 'foo', callbacks: [ @@ -592,7 +620,6 @@ export const redirectCallback = { { name: 'redirectUrl', value: - // eslint-disable-next-line max-len 'http://localhost:9443/o/oauth2/v2/auth?nonce=ko7fdf2v3b6yctgq35bdpndel0p9qiq&response_type=code&client_id=546064052569-ke17g9ufsmvda3kgg7s5kp2hpf3gnqi8.apps.googleusercontent.com&scope=openid%20profile%20email&code_challenge=Bh_6aMiI04KGI1wVILtEamByklmXnQY9JKhKhlwsIxk&code_challenge_method=S256&state=rtu8pz65dbg6baw985d532myfbbnf5v', }, { name: 'redirectMethod', value: 'GET' }, @@ -609,9 +636,7 @@ export const redirectCallbackSaml = { output: [ { name: 'redirectUrl', - value: - // eslint-disable-next-line max-len - 'http://localhost:9443/SAMLTest/', + value: 'http://localhost:9443/SAMLTest/', }, { name: 'redirectMethod', value: 'GET' }, { name: 'trackingCookie', value: true }, @@ -627,9 +652,7 @@ export const redirectCallbackFailureSaml = { output: [ { name: 'redirectUrl', - value: - // eslint-disable-next-line max-len - 'http://localhost:9443/SAMLFailure', + value: 'http://localhost:9443/SAMLFailure', }, { name: 'redirectMethod', value: 'GET' }, { name: 'trackingCookie', value: true }, diff --git a/e2e/am-mock-api/src/app/routes.auth.js b/e2e/am-mock-api/src/app/routes.auth.js index 52a2fd143d1..046d41c8e43 100644 --- a/e2e/am-mock-api/src/app/routes.auth.js +++ b/e2e/am-mock-api/src/app/routes.auth.js @@ -3,7 +3,7 @@ * * routes.auth.js * - * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * Copyright (c) 2020 - 2026 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. */ @@ -28,6 +28,7 @@ import { pollingCallback, pingProtectEvaluate, pingProtectInitialize, + pingProtectSignalsInitializationOptions, redirectCallback, redirectCallbackSaml, requestDeviceProfile, @@ -42,7 +43,6 @@ import { textInputCallback, treeAuthz, txnAuthz, - otpQRCodeCallbacks, wellKnownForgeRock, recaptchaEnterpriseCallback, MetadataMarketPlaceInitialize, @@ -94,6 +94,11 @@ export default function (app) { res.json(nameCallback); } else if (req.query.authIndexValue === 'TEST_LoginPingProtect') { res.json({ ...pingProtectInitialize, authId: 'protect-journey-init' }); + } else if (req.query.authIndexValue === 'TEST_LoginPingProtectSignalsOptions') { + res.json({ + ...pingProtectSignalsInitializationOptions, + authId: 'protect-journey-signals-init-options', + }); } else if (req.query.authIndexValue === 'IDMSocialLogin') { res.json(selectIdPCallback); } else if (req.query.authIndexValue === 'TEST_MetadataMarketPlace') { @@ -449,10 +454,8 @@ export default function (app) { }); app.post(authPaths.tokenExchange, wait, async (req, res) => { - // eslint-disable-next-line const access_token = v4(); const refresh_token = v4(); - // eslint-disable-next-line const tokens = { ...oauthTokens, access_token, refresh_token }; if (req.path.includes('middleware')) { @@ -496,7 +499,6 @@ export default function (app) { } else { const referrer = new URL(req.get('Referer')); const additionalQueryParams = - // eslint-disable-next-line max-len 'state=rtu8pz65dbg6baw985d532myfbbnf5v&code=4%2F0AY0e-g5vHGhzfggdAuIofxnblW-iR1Y30G5lN5RvbrU8Zv5ZmtUVheTzSX7YMJF_usbzUA&scope=email+profile+openid+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile&authuser=0&hd=forgerock.com&prompt=none'; const redirectUrl = `${referrer.href}${ referrer.href.includes('?') ? '&' : '?' diff --git a/e2e/journey-app/components/ping-protect-initialize.ts b/e2e/journey-app/components/ping-protect-initialize.ts index 6cbe21db28d..a42c9c4744c 100644 --- a/e2e/journey-app/components/ping-protect-initialize.ts +++ b/e2e/journey-app/components/ping-protect-initialize.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * Copyright (c) 2025 - 2026 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. @@ -43,19 +43,8 @@ export default function pingProtectInitializeComponent( 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 }); + protectInstance = protect(config); console.log('Protect instance created'); // Initialize the Protect SDK diff --git a/e2e/journey-suites/src/protect.test.ts b/e2e/journey-suites/src/protect.test.ts index 5879dc4c135..1cc07249cab 100644 --- a/e2e/journey-suites/src/protect.test.ts +++ b/e2e/journey-suites/src/protect.test.ts @@ -10,76 +10,177 @@ import { asyncEvents } from './utils/async-events.js'; import { username, password } from './utils/demo-user.js'; import type { Callback, NameValue } from '@forgerock/journey-client'; -test('Test PingOne Protect journey flow', async ({ page }) => { - const { clickButton } = asyncEvents(page); - const messageArray: string[] = []; +test.describe('Test PingOne Protect journey flow', () => { + test('initialized with envId (standard config)', async ({ page }) => { + const { clickButton } = asyncEvents(page); + const messageArray: string[] = []; - page.on('console', async (msg) => { - messageArray.push(msg.text()); - return Promise.resolve(true); - }); + page.on('console', async (msg) => { + messageArray.push(msg.text()); + return Promise.resolve(true); + }); - let riskData: string | null = null; + let riskData: string | null = null; + let envIdConfig: string | null = null; - page.on('request', (request) => { - const method = request.method(); - const requestUrl = request.url(); - const payload = request.postDataJSON(); + page.on('request', (request) => { + const method = request.method(); + const requestUrl = request.url(); + const payload = request.postDataJSON(); - // Only process POST requests with JSON payloads - if (method === 'POST' && payload && requestUrl.includes('/authenticate')) { - const callback: Callback = payload.callbacks?.find( - (callback: Callback) => callback.type === 'PingOneProtectEvaluationCallback', - ); + // Only process POST requests with JSON payloads + if (method === 'POST' && payload && requestUrl.includes('/authenticate')) { + const protectCb: Callback = payload.callbacks?.find( + (callback: Callback) => + callback.type === 'PingOneProtectInitializeCallback' || + callback.type === 'PingOneProtectEvaluationCallback', + ); - if (callback) { - const data = callback.input?.find((input: NameValue) => input.name === 'IDToken1signals') - ?.value as string | undefined; - riskData = data ?? null; + if (protectCb?.type === 'PingOneProtectInitializeCallback') { + const envId = protectCb.output?.find((output: NameValue) => output.name === 'envId') + ?.value as string | undefined; + envIdConfig = envId ?? null; + } + + if (protectCb?.type === 'PingOneProtectEvaluationCallback') { + const data = protectCb.input?.find((input: NameValue) => input.name === 'IDToken1signals') + ?.value as string | undefined; + riskData = data ?? null; + } } - } - }); + }); - await page.goto('/?journey=TEST_LoginPingProtect&clientId=basic'); + await page.goto('/?journey=TEST_LoginPingProtect&clientId=basic'); - await expect(page.getByText('Initializing PingOne Protect...')).toBeVisible({ timeout: 10000 }); - await expect(page.getByText('PingOne Protect initialized successfully!')).toBeVisible({ - timeout: 15000, - }); + await expect(page.getByText('Initializing PingOne Protect...')).toBeVisible({ timeout: 10000 }); + await expect(page.getByText('PingOne Protect initialized successfully!')).toBeVisible({ + timeout: 15000, + }); + + const protectPromise = page.waitForRequest((req) => { + return ( + req.method() === 'POST' && + req.url().includes('/authenticate') && + req + .postDataJSON() + ?.callbacks?.some( + (callback: Callback) => callback.type === 'PingOneProtectEvaluationCallback', + ) + ); + }); - const protectPromise = page.waitForRequest((req) => { - return ( - req.method() === 'POST' && - req.url().includes('/authenticate') && - req - .postDataJSON() - ?.callbacks?.some( - (callback: Callback) => callback.type === 'PingOneProtectEvaluationCallback', - ) + await expect(page.getByLabel('User Name')).toBeVisible(); + await page.getByLabel('User Name').fill(username); + await page.getByLabel('Password').fill(password); + await clickButton('Submit', '/authenticate'); + + await expect(page.getByText('Evaluating risk assessment...')).toBeVisible({ timeout: 10000 }); + await expect(page.getByText('Risk assessment completed successfully!')).toBeVisible({ + timeout: 15000, + }); + + // Wait for risk data to be evaluated + await protectPromise; + + // Verify signals were captured from the request + expect(riskData).not.toBeNull(); + expect(typeof riskData).toBe('string'); + expect(riskData).toMatch(/^R\/o\//); + + // Verify standard config by presence of envId + expect(envIdConfig).not.toBeNull(); + expect(typeof envIdConfig).toBe('string'); + + // 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, ); }); - await expect(page.getByLabel('User Name')).toBeVisible(); - await page.getByLabel('User Name').fill(username); - await page.getByLabel('Password').fill(password); - await clickButton('Submit', '/authenticate'); + test('intialized with signalInitializationOptions', async ({ page }) => { + const { clickButton } = asyncEvents(page); + const messageArray: string[] = []; - await expect(page.getByText('Evaluating risk assessment...')).toBeVisible({ timeout: 10000 }); - await expect(page.getByText('Risk assessment completed successfully!')).toBeVisible({ - timeout: 15000, - }); + page.on('console', async (msg) => { + messageArray.push(msg.text()); + return Promise.resolve(true); + }); + + let riskData: string | null = null; + let envIdConfig: string | null = null; - // Wait for risk data to be evaluated - await protectPromise; + page.on('request', (request) => { + const method = request.method(); + const requestUrl = request.url(); + const payload = request.postDataJSON(); - // Verify signals were captured from the request - expect(riskData).not.toBeNull(); - expect(typeof riskData).toBe('string'); - expect(riskData).toMatch(/^R\/o\//); + // Only process POST requests with JSON payloads + if (method === 'POST' && payload && requestUrl.includes('/authenticate')) { + const protectCb: Callback = payload.callbacks?.find( + (callback: Callback) => + callback.type === 'PingOneProtectInitializeCallback' || + callback.type === 'PingOneProtectEvaluationCallback', + ); - // 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, - ); + if (protectCb?.type === 'PingOneProtectInitializeCallback') { + const envId = protectCb.output?.find((output: NameValue) => output.name === 'envId') + ?.value as string | undefined; + envIdConfig = envId ?? null; + } + + if (protectCb?.type === 'PingOneProtectEvaluationCallback') { + const data = protectCb.input?.find((input: NameValue) => input.name === 'IDToken1signals') + ?.value as string | undefined; + riskData = data ?? null; + } + } + }); + + await page.goto('/?journey=TEST_LoginPingProtectSignalsOptions&clientId=basic'); + + await expect(page.getByText('Initializing PingOne Protect...')).toBeVisible({ timeout: 10000 }); + await expect(page.getByText('PingOne Protect initialized successfully!')).toBeVisible({ + timeout: 15000, + }); + + const protectPromise = page.waitForRequest((req) => { + return ( + req.method() === 'POST' && + req.url().includes('/authenticate') && + req + .postDataJSON() + ?.callbacks?.some( + (callback: Callback) => callback.type === 'PingOneProtectEvaluationCallback', + ) + ); + }); + + await expect(page.getByLabel('User Name')).toBeVisible(); + await page.getByLabel('User Name').fill(username); + await page.getByLabel('Password').fill(password); + await clickButton('Submit', '/authenticate'); + + await expect(page.getByText('Evaluating risk assessment...')).toBeVisible({ timeout: 10000 }); + await expect(page.getByText('Risk assessment completed successfully!')).toBeVisible({ + timeout: 15000, + }); + + // Wait for risk data to be evaluated + await protectPromise; + + // Verify signals were captured from the request + expect(riskData).not.toBeNull(); + expect(typeof riskData).toBe('string'); + expect(riskData).toMatch(/^R\/o\//); + + // Verify signalInitializationOptions config by absence of envId + expect(envIdConfig).toBeNull(); + + // 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, + ); + }); });