From bb7d1d743dfedc4e6504e7beef15d04923b36dbb Mon Sep 17 00:00:00 2001 From: AJ Ancheta <7781450+ancheetah@users.noreply.github.com> Date: Sun, 26 Apr 2026 22:07:46 -0400 Subject: [PATCH 1/2] fix(reactjs-todo-oidc): support P1 user responses and align sample apps (SDKS-4902) --- .github/workflows/CI.yml | 3 + javascript/package-lock.json | 268 ------------------ javascript/reactjs-todo-davinci/.env.example | 15 +- javascript/reactjs-todo-davinci/README.md | 9 +- .../client/context/README.md | 5 + .../reactjs-todo-davinci/client/views/home.js | 4 +- .../e2e/davinci-login.spec.js | 4 +- .../e2e/davinci-logout.spec.js | 6 +- .../e2e/utils/demo-user.js | 1 + .../reactjs-todo-davinci/webpack.config.js | 2 +- javascript/reactjs-todo-journey/.env.example | 10 +- javascript/reactjs-todo-journey/README.md | 22 +- .../reactjs-todo-journey/client/constants.js | 2 - .../client/context/README.md | 5 + .../client/views/home.jsx | 2 +- .../client/views/login.jsx | 49 ++-- javascript/reactjs-todo-journey/package.json | 3 +- .../reactjs-todo-journey/vite.config.js | 13 + .../reactjs-todo-login-widget/.env.example | 21 +- .../reactjs-todo-login-widget/README.md | 32 +-- .../client/constants.js | 2 - .../client/context/README.md | 5 + .../client/context/auth.context.js | 10 +- .../playwright.config.ts | 24 +- .../webpack.config.js | 7 +- javascript/reactjs-todo-oidc/.env.example | 15 +- javascript/reactjs-todo-oidc/README.md | 56 ++-- javascript/reactjs-todo-oidc/client/README.md | 14 +- .../reactjs-todo-oidc/client/constants.js | 5 +- .../client/context/README.md | 5 + .../client/context/protect.context.js | 69 ----- javascript/reactjs-todo-oidc/client/index.js | 16 +- .../reactjs-todo-oidc/client/views/home.js | 6 +- .../reactjs-todo-oidc/client/views/login.js | 112 ++++---- ...ogin.spec.js => oidc-login-pingam.spec.js} | 6 +- .../e2e/oidc-login-pingone.spec.js | 85 ++++++ .../reactjs-todo-oidc/e2e/oidc-todo.spec.js | 6 +- .../reactjs-todo-oidc/e2e/utils/demo-user.js | 10 +- .../reactjs-todo-oidc/playwright.config.ts | 26 +- .../reactjs-todo-oidc/webpack.config.js | 17 +- 40 files changed, 387 insertions(+), 585 deletions(-) create mode 100644 javascript/reactjs-todo-davinci/client/context/README.md create mode 100644 javascript/reactjs-todo-journey/client/context/README.md create mode 100644 javascript/reactjs-todo-login-widget/client/context/README.md create mode 100644 javascript/reactjs-todo-oidc/client/context/README.md delete mode 100644 javascript/reactjs-todo-oidc/client/context/protect.context.js rename javascript/reactjs-todo-oidc/e2e/{oidc-login.spec.js => oidc-login-pingam.spec.js} (96%) create mode 100644 javascript/reactjs-todo-oidc/e2e/oidc-login-pingone.spec.js diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index e2d15c4a..682c8781 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -42,6 +42,7 @@ jobs: (cd reactjs-todo-journey && npm run e2e -- --shard=${{ matrix.shardIndex }}/4) (cd reactjs-todo-oidc && npm run e2e -- --shard=${{ matrix.shardIndex }}/4) (cd reactjs-todo-davinci && npm run e2e -- --shard=${{ matrix.shardIndex }}/4) + (cd reactjs-todo-login-widget && npm run e2e -- --shard=${{ matrix.shardIndex }}/4) env: REST_OAUTH_SECRET: ${{ secrets.REST_OAUTH_SECRET }} @@ -56,3 +57,5 @@ jobs: ./javascript/reactjs-todo-oidc/playwright-report ./javascript/reactjs-todo-davinci/test-results ./javascript/reactjs-todo-davinci/playwright-report + ./javascript/reactjs-todo-login-widget/test-results + ./javascript/reactjs-todo-login-widget/playwright-report diff --git a/javascript/package-lock.json b/javascript/package-lock.json index 61cbff5e..84695415 100644 --- a/javascript/package-lock.json +++ b/javascript/package-lock.json @@ -11373,28 +11373,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.0.tgz", - "integrity": "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==", - "extraneous": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.58.0", - "@typescript-eslint/types": "^8.58.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, "node_modules/@typescript-eslint/scope-manager": { "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", @@ -11427,23 +11405,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.0.tgz", - "integrity": "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==", - "extraneous": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, "node_modules/@typescript-eslint/type-utils": { "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", @@ -11509,20 +11470,6 @@ "eslint": "^8.56.0" } }, - "node_modules/@typescript-eslint/types": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz", - "integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==", - "extraneous": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@typescript-eslint/typescript-estree": { "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", @@ -11566,172 +11513,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/utils": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.0.tgz", - "integrity": "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==", - "extraneous": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.58.0", - "@typescript-eslint/types": "8.58.0", - "@typescript-eslint/typescript-estree": "8.58.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.0.tgz", - "integrity": "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==", - "extraneous": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.58.0", - "@typescript-eslint/visitor-keys": "8.58.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.0.tgz", - "integrity": "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==", - "extraneous": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.58.0", - "@typescript-eslint/tsconfig-utils": "8.58.0", - "@typescript-eslint/types": "8.58.0", - "@typescript-eslint/visitor-keys": "8.58.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.0.tgz", - "integrity": "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==", - "extraneous": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.58.0", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "extraneous": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "extraneous": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "extraneous": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "extraneous": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "extraneous": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "extraneous": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, "node_modules/@typescript-eslint/visitor-keys": { "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", @@ -17277,24 +17058,6 @@ "node": ">=0.8.0" } }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "extraneous": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/fetch-cookie": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-0.11.0.tgz", @@ -27875,36 +27638,6 @@ "dev": true, "license": "MIT" }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "extraneous": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "extraneous": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -30303,7 +30036,6 @@ "dependencies": { "@forgerock/journey-client": "latest", "@forgerock/oidc-client": "latest", - "@forgerock/protect": "latest", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.19.0" diff --git a/javascript/reactjs-todo-davinci/.env.example b/javascript/reactjs-todo-davinci/.env.example index 35caf190..65c7e725 100644 --- a/javascript/reactjs-todo-davinci/.env.example +++ b/javascript/reactjs-todo-davinci/.env.example @@ -1,13 +1,14 @@ -API_URL= -DEBUGGER_OFF=true -DEVELOPMENT= -PORT= +WELLKNOWN_URL= WEB_OAUTH_CLIENT= +API_URL=http://localhost:9443 +DEBUGGER_OFF=true +DEVELOPMENT=true +PORT=8443 SCOPE="openid profile email phone name revoke" -WELLKNOWN_URL= -# bootstrap | flow + +# INIT_PROTECT (optional) - bootstrap | flow # 'bootstrap' will initialize protect at app bootstrap time # 'flow' relies on the PingOne Protect collector for initialization INIT_PROTECT= -# required when ProtectCollector is present +# PINGONE_ENV_ID - required when ProtectCollector is present PINGONE_ENV_ID= diff --git a/javascript/reactjs-todo-davinci/README.md b/javascript/reactjs-todo-davinci/README.md index 42c228fd..9f9d7c11 100644 --- a/javascript/reactjs-todo-davinci/README.md +++ b/javascript/reactjs-todo-davinci/README.md @@ -57,14 +57,13 @@ Change the name of `.env.example` to `.env` and replace the dummy values (e.g. ` Example with annotations: ```text +WELLKNOWN_URL=<> +WEB_OAUTH_CLIENT=<> API_URL=http://localhost:9443 DEBUGGER_OFF=true DEVELOPMENT=true PORT=8443 -CLIENT_ID=<> -REDIRECT_URI=http://localhost:8443 SCOPE="openid profile email phone name revoke" -WELLKNOWN_URL=<>/ ``` ### Installing Dependencies @@ -72,7 +71,7 @@ WELLKNOWN_URL=<>/ **Run from root of `/javascript`**: since this sample app uses npm's workspaces, we recommend running the npm commands from the root of the `/javascript` folder. ```sh -# Install all dependencies (no need to pass the -w option) +# Install all dependencies npm install ``` @@ -81,7 +80,7 @@ npm install Now, run the below commands to start the processes needed for building the application and running the servers for both client and API server: ```sh -# In one terminal window, run the following watch command from the root of the repository +# In a terminal window, run the following command from the /javascript folder npm run start:reactjs-todo-dv ``` diff --git a/javascript/reactjs-todo-davinci/client/context/README.md b/javascript/reactjs-todo-davinci/client/context/README.md new file mode 100644 index 00000000..7a01fd20 --- /dev/null +++ b/javascript/reactjs-todo-davinci/client/context/README.md @@ -0,0 +1,5 @@ +# React Context + +This application leverages "global state" with React's Context API. This can be useful to share state with any component without having to pass props through deeply nested components. Authentication status and theme state are good examples. + +If global state becomes a more complex function of the app, something like Redux might be a better option. \ No newline at end of file diff --git a/javascript/reactjs-todo-davinci/client/views/home.js b/javascript/reactjs-todo-davinci/client/views/home.js index 8701734a..43d01a8e 100755 --- a/javascript/reactjs-todo-davinci/client/views/home.js +++ b/javascript/reactjs-todo-davinci/client/views/home.js @@ -67,7 +67,7 @@ export default function Home() { library, our{' '} JavaScript SDK{' '} @@ -88,7 +88,7 @@ export default function Home() { this project can be found on Github {' '} and run locally for experimentation. For more on our SDKs,{' '} - + you can find our official SDK documentation here.

diff --git a/javascript/reactjs-todo-davinci/e2e/davinci-login.spec.js b/javascript/reactjs-todo-davinci/e2e/davinci-login.spec.js index 14a6c4c5..b6ec8c32 100644 --- a/javascript/reactjs-todo-davinci/e2e/davinci-login.spec.js +++ b/javascript/reactjs-todo-davinci/e2e/davinci-login.spec.js @@ -6,7 +6,7 @@ * */ import { test, expect } from '@playwright/test'; -import { username, password } from './utils/demo-user'; +import { username, password, displayName } from './utils/demo-user'; const BASE_URL = 'http://localhost:8443'; @@ -17,7 +17,7 @@ test.describe('React - DaVinci Login', () => { await page.getByLabel('Username').fill(username); await page.getByLabel('Password').fill(password); await page.getByRole('button', { name: 'Sign On' }).click(); - await expect(page.getByText('Welcome back, JS DaVinci Sample Apps E2E!')).toBeVisible(); + await expect(page.getByText(`Welcome back, ${displayName}!`)).toBeVisible(); await expect(page.getByText('Protect with Ping')).toBeVisible(); }); test('Login with invalid credentials, fail', async ({ page }) => { diff --git a/javascript/reactjs-todo-davinci/e2e/davinci-logout.spec.js b/javascript/reactjs-todo-davinci/e2e/davinci-logout.spec.js index ffadc74b..791a1020 100644 --- a/javascript/reactjs-todo-davinci/e2e/davinci-logout.spec.js +++ b/javascript/reactjs-todo-davinci/e2e/davinci-logout.spec.js @@ -6,7 +6,7 @@ * */ import { test, expect } from '@playwright/test'; -import { username, password } from './utils/demo-user'; +import { username, password, displayName } from './utils/demo-user'; const BASE_URL = 'http://localhost:8443'; @@ -18,7 +18,7 @@ test.describe('React - DaVinci Logout', () => { await page.getByLabel('Username').fill(username); await page.getByLabel('Password').fill(password); await page.getByRole('button', { name: 'Sign On' }).click(); - await expect(page.getByText('Welcome back, JS DaVinci Sample Apps E2E!')).toBeVisible(); + await expect(page.getByText(`Welcome back, ${displayName}!`)).toBeVisible(); await expect(page.getByText('Protect with Ping')).toBeVisible(); // Logout @@ -26,7 +26,7 @@ test.describe('React - DaVinci Logout', () => { await page.getByRole('link', { name: 'Sign Out' }).click(); await page.waitForURL(BASE_URL + '/logout'); await page.waitForURL(BASE_URL); - await expect(page.getByText('Welcome back')).not.toBeVisible(); + await expect(page.getByText(`Welcome back, ${displayName}!`)).not.toBeVisible(); await expect(page.getByText('Protect with Ping')).toBeVisible(); }); }); diff --git a/javascript/reactjs-todo-davinci/e2e/utils/demo-user.js b/javascript/reactjs-todo-davinci/e2e/utils/demo-user.js index 6defda7d..19772b78 100644 --- a/javascript/reactjs-todo-davinci/e2e/utils/demo-user.js +++ b/javascript/reactjs-todo-davinci/e2e/utils/demo-user.js @@ -7,3 +7,4 @@ */ export const username = 'JsDvSampleAppsE2E@user.com'; export const password = 'Demo_12345!'; +export const displayName = 'JS DaVinci Sample Apps E2E'; diff --git a/javascript/reactjs-todo-davinci/webpack.config.js b/javascript/reactjs-todo-davinci/webpack.config.js index 85cb478f..2c47ea44 100644 --- a/javascript/reactjs-todo-davinci/webpack.config.js +++ b/javascript/reactjs-todo-davinci/webpack.config.js @@ -8,10 +8,10 @@ module.exports = () => { const localEnv = dotenv.config().parsed || {}; // Use process environment variables for prod, but fallback to local .env for dev + const PORT = process.env.PORT || localEnv.PORT || '8443'; const API_URL = process.env.API_URL || localEnv.API_URL; const DEBUGGER_OFF = process.env.DEBUGGER_OFF || localEnv.DEBUGGER_OFF; const DEVELOPMENT = process.env.DEVELOPMENT || localEnv.DEVELOPMENT; - const PORT = process.env.PORT || localEnv.PORT; const WEB_OAUTH_CLIENT = process.env.WEB_OAUTH_CLIENT || localEnv.WEB_OAUTH_CLIENT; const SCOPE = process.env.SCOPE || localEnv.SCOPE; const WELLKNOWN_URL = process.env.WELLKNOWN_URL || localEnv.WELLKNOWN_URL; diff --git a/javascript/reactjs-todo-journey/.env.example b/javascript/reactjs-todo-journey/.env.example index cfe7dc8a..b8d3e61b 100644 --- a/javascript/reactjs-todo-journey/.env.example +++ b/javascript/reactjs-todo-journey/.env.example @@ -1,14 +1,14 @@ # VITE_APP_URL - not using this for preview-environment instead, we can use window.location.origin VITE_WELLKNOWN_URL= -VITE_SCOPE= -VITE_API_URL= +VITE_WEB_OAUTH_CLIENT= +VITE_API_URL=http://localhost:9443 VITE_PORT=8443 VITE_DEBUGGER_OFF=true VITE_DEVELOPMENT=true +VITE_SCOPE='openid profile email' VITE_JOURNEY_LOGIN=Login -VITE_JOURNEY_REGISTER= -VITE_WEB_OAUTH_CLIENT= -VITE_REALM_PATH= +VITE_JOURNEY_REGISTER=Registration +VITE_REALM_PATH=alpha VITE_CENTRALIZED_LOGIN=false # VITE_INIT_PROTECT (optional) - bootstrap | journey diff --git a/javascript/reactjs-todo-journey/README.md b/javascript/reactjs-todo-journey/README.md index 651b4df3..1716e07b 100644 --- a/javascript/reactjs-todo-journey/README.md +++ b/javascript/reactjs-todo-journey/README.md @@ -63,15 +63,17 @@ Change the name of `.env.example` to `.env` and fill the environment variables w Example with annotations: ```text -WELLKNOWN_URL=<<>> -APP_URL=https://localhost:8443 # in develop we do not use this variable for dynamic deployment reasons -API_URL=http://localhost:9443 -DEBUGGER_OFF=false -JOURNEY_LOGIN=Login -JOURNEY_REGISTER=Registration -REALM_PATH=<<>> -WEB_OAUTH_CLIENT=<<>> -SCOPE='openid profile email' +VITE_APP_URL=https://localhost:8443 # in develop we do not use this variable for dynamic deployment reasons +VITE_WELLKNOWN_URL=<<>> +VITE_WEB_OAUTH_CLIENT=<<>> +VITE_API_URL=http://localhost:9443 +VITE_PORT=8443 +VITE_DEBUGGER_OFF=true +VITE_DEVELOPMENT=true +VITE_SCOPE='openid profile email' +VITE_JOURNEY_LOGIN=Login +VITE_JOURNEY_REGISTER=Registration +VITE_REALM_PATH=<<>> ``` ### Installing Dependencies and Run Build @@ -79,7 +81,7 @@ SCOPE='openid profile email' **Run from `/javascript` root**: Since this sample app uses npm's workspaces, we recommend running the npm commands from the root of the `/javascript` folder. ```sh -# Install all dependencies (no need to pass the -w option) +# Install all dependencies npm install ``` diff --git a/javascript/reactjs-todo-journey/client/constants.js b/javascript/reactjs-todo-journey/client/constants.js index a3656d74..4c90c4ae 100755 --- a/javascript/reactjs-todo-journey/client/constants.js +++ b/javascript/reactjs-todo-journey/client/constants.js @@ -11,7 +11,6 @@ const urlParams = new URLSearchParams(window.location.search); const centralLoginParam = urlParams.get('centralLogin'); -export const SERVER_URL = import.meta.env.VITE_SERVER_URL; export const API_URL = import.meta.env.VITE_API_URL; // Yes, the debugger boolean is intentionally reversed export const DEBUGGER = import.meta.env.VITE_DEBUGGER_OFF === 'false'; @@ -20,7 +19,6 @@ export const JOURNEY_REGISTER = import.meta.env.VITE_JOURNEY_REGISTER; export const WEB_OAUTH_CLIENT = import.meta.env.VITE_WEB_OAUTH_CLIENT; export const REALM_PATH = import.meta.env.VITE_REALM_PATH; export const CENTRALIZED_LOGIN = import.meta.env.VITE_CENTRALIZED_LOGIN; -export const SESSION_URL = `${SERVER_URL}json/realms/root/sessions`; export const SCOPE = import.meta.env.VITE_SCOPE; export const WELLKNOWN_URL = import.meta.env.VITE_WELLKNOWN_URL; export const INIT_PROTECT = import.meta.env.VITE_INIT_PROTECT; diff --git a/javascript/reactjs-todo-journey/client/context/README.md b/javascript/reactjs-todo-journey/client/context/README.md new file mode 100644 index 00000000..7a01fd20 --- /dev/null +++ b/javascript/reactjs-todo-journey/client/context/README.md @@ -0,0 +1,5 @@ +# React Context + +This application leverages "global state" with React's Context API. This can be useful to share state with any component without having to pass props through deeply nested components. Authentication status and theme state are good examples. + +If global state becomes a more complex function of the app, something like Redux might be a better option. \ No newline at end of file diff --git a/javascript/reactjs-todo-journey/client/views/home.jsx b/javascript/reactjs-todo-journey/client/views/home.jsx index 61f9dd0f..19319948 100755 --- a/javascript/reactjs-todo-journey/client/views/home.jsx +++ b/javascript/reactjs-todo-journey/client/views/home.jsx @@ -81,7 +81,7 @@ export default function Home() { this project can be found on Github {' '} and run locally for experimentation. For more on our SDKs,{' '} - + you can find our official SDK documentation here.

diff --git a/javascript/reactjs-todo-journey/client/views/login.jsx b/javascript/reactjs-todo-journey/client/views/login.jsx index d48d50d9..42b89db7 100755 --- a/javascript/reactjs-todo-journey/client/views/login.jsx +++ b/javascript/reactjs-todo-journey/client/views/login.jsx @@ -7,7 +7,7 @@ * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ -import { useContext, useEffect, useState } from 'react'; +import { useContext, useEffect, useState, useCallback } from 'react'; import { Link, useSearchParams, useNavigate } from 'react-router-dom'; import BackHome from '../components/utilities/back-home'; import Loading from '../components/utilities/loading'; @@ -40,12 +40,10 @@ export default function Login() { // Get environment variable const isCentralizedLogin = CENTRALIZED_LOGIN === 'true' || centralLogin === 'true'; - const [state, setState] = useState({ - loadingMessage: '', - }); + const [loadingMessage, setLoadingMessage] = useState(''); - useEffect(() => { - async function authorize(codeParam, stateParam) { + const authorize = useCallback( + async function authorizeCallback(codeParam, stateParam) { /** ***************************************************************** * SDK INTEGRATION POINT * Summary: Get OAuth/OIDC tokens and user info @@ -57,20 +55,27 @@ export default function Login() { const tokenResponse = await oidcClient.token.exchange(codeParam, stateParam); if ('error' in tokenResponse) { + setLoadingMessage('Sign in failed. Please try again.'); console.error('Token exchange error:', tokenResponse); + return; } const user = await oidcClient.user.info(); if ('error' in user) { + setLoadingMessage('Sign in failed. Please try again.'); console.error('Error getting user:', user); + return; } methods.setUser(user.name); methods.setEmail(user.email); methods.setAuthentication(true); navigate('/'); - } + }, + [oidcClient, methods, navigate], + ); + useEffect(() => { async function checkCentralizedLogin() { if (isCentralizedLogin) { if (codeParam && stateParam) { @@ -79,12 +84,10 @@ export default function Login() { * the URL will include code and state query parameters that need to * be passed in to complete the OAuth flow giving the user access */ - setState({ - loadingMessage: 'Success! Redirecting ...', - }); + setLoadingMessage('Success! Redirecting ...'); await authorize(codeParam, stateParam); } else if (errorParam) { - // Do nothing as it will redirect for central login + setLoadingMessage('Sign in failed. Please try again.'); } else { /** ***************************************************************** * SDK INTEGRATION POINT @@ -95,21 +98,28 @@ export default function Login() { ***************************************************************** */ if (DEBUGGER) debugger; - setState({ - loadingMessage: 'Redirecting ...', - }); + setLoadingMessage('Redirecting ...'); const authorizeUrl = await oidcClient.authorize.url(); if (typeof authorizeUrl !== 'string' && 'error' in authorizeUrl) { + setLoadingMessage('Sign in failed. Please try again.'); console.error('Authorization URL Error:', authorizeUrl); - } else { - window.location.assign(authorizeUrl); + return; } + + window.location.assign(authorizeUrl); } } } checkCentralizedLogin(); - }, [codeParam, errorParam, isCentralizedLogin, methods, navigate, oidcClient, stateParam]); + }, [ + authorize, + codeParam, + errorParam, + isCentralizedLogin, + oidcClient, + stateParam, + ]); if (!isCentralizedLogin) { return ( @@ -134,7 +144,10 @@ export default function Login() { return (
- + + + +
); diff --git a/javascript/reactjs-todo-journey/package.json b/javascript/reactjs-todo-journey/package.json index d7384c77..f60c3c5e 100644 --- a/javascript/reactjs-todo-journey/package.json +++ b/javascript/reactjs-todo-journey/package.json @@ -22,7 +22,6 @@ "dependencies": { "@forgerock/journey-client": "latest", "@forgerock/oidc-client": "latest", - "@forgerock/protect": "latest", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.19.0" @@ -30,7 +29,7 @@ "scripts": { "start": "vite", "build": "vite build", - "lint": "eslint .", + "lint": "eslint . --fix", "preview": "vite preview", "e2e": "playwright test", "e2e:ui": "playwright test --ui", diff --git a/javascript/reactjs-todo-journey/vite.config.js b/javascript/reactjs-todo-journey/vite.config.js index 920a1b30..dc6e2562 100644 --- a/javascript/reactjs-todo-journey/vite.config.js +++ b/javascript/reactjs-todo-journey/vite.config.js @@ -25,5 +25,18 @@ export default defineConfig(({ mode }) => { strictPort: true, }, plugins: [react()], + css: { + preprocessorOptions: { + scss: { + silenceDeprecations: [ + 'legacy-js-api', + 'import', + 'if-function', + 'global-builtin', + 'color-functions', + ], + }, + }, + }, }; }); diff --git a/javascript/reactjs-todo-login-widget/.env.example b/javascript/reactjs-todo-login-widget/.env.example index ff73e679..33137f40 100644 --- a/javascript/reactjs-todo-login-widget/.env.example +++ b/javascript/reactjs-todo-login-widget/.env.example @@ -1,17 +1,14 @@ # APP_URL= # not using this for preview-environment instead, we can use window.location.origin SERVER_URL= -SCOPE= -API_URL= -DEBUGGER_OFF=true -DEVELOPMENT= -JOURNEY_LOGIN= -JOURNEY_REGISTER= -PORT= -REALM_PATH= -REST_OAUTH_CLIENT= -REST_OAUTH_SECRET= WEB_OAUTH_CLIENT= -CENTRALIZED_LOGIN= -SERVER_TYPE= +SCOPE='openid profile email' +PORT=8443 +API_URL=http://localhost:9443 +DEBUGGER_OFF=true +DEVELOPMENT=true +JOURNEY_LOGIN=Login +JOURNEY_REGISTER=Registration +REALM_PATH=alpha + # required when PingOne Protect callbacks are present PINGONE_ENV_ID= diff --git a/javascript/reactjs-todo-login-widget/README.md b/javascript/reactjs-todo-login-widget/README.md index 3f6002ef..5d1da1f7 100644 --- a/javascript/reactjs-todo-login-widget/README.md +++ b/javascript/reactjs-todo-login-widget/README.md @@ -9,7 +9,7 @@ This sample code is provided "as is" and is not a supported product of Ping. It' 1. An instance of Ping's Access Manager (AM), either within a Ping's Advanced Identity Cloud tenant, your own private installation or locally installed on your computer 2. Node >= 24.2.0 (recommended: install via [official package installer](https://nodejs.org/en/)) 3. Knowledge of using the Terminal/Command Line -4. Ability to generate security certs (recommended: mkcert ([installation instructions here](https://github.com/FiloSottile/mkcert#installation)) +4. Ability to generate security certs (recommended: mkcert ([installation instructions here](https://github.com/FiloSottile/mkcert#installation))) 5. This project "cloned" to your computer ## Setup @@ -35,21 +35,6 @@ Once you have the 5 requirements above met, we can build the project. 1. Login 2. Register -Note: The sample app currently supports the following callbacks only: - -- NameCallback -- PasswordCallback -- ChoiceCallback -- ValidatedCreateUsernameCallback -- ValidatedCreatePasswordCallback -- StringAttributeInputCallback -- BooleanAttributeInputCallback -- KbaCreateCallback -- TermsAndConditionsCallback -- TextOutputCallback -- ConfirmationCallback -- SelectIdPCallback - ### Configure Your `.env` File Change the name of `.env.example` to `.env` and replace the bracketed values (e.g. `<<>>`) with your values. @@ -57,22 +42,25 @@ Change the name of `.env.example` to `.env` and replace the bracketed values (e. Example with annotations: ```text -SERVER_URL=<<>> APP_URL=https://localhost:8443 # in develop we do not use this variable for dynamic deployment reasons +SERVER_URL=<<>> +WEB_OAUTH_CLIENT=<<>> +SCOPE='openid profile email' +PORT=8443 API_URL=http://localhost:9443 -DEBUGGER_OFF=false +DEBUGGER_OFF=true +DEVELOPMENT=true JOURNEY_LOGIN=Login JOURNEY_REGISTER=Registration REALM_PATH=<<>> -WEB_OAUTH_CLIENT=<<>> ``` ### Installing Dependencies and Run Build -**Run from root of repo**: since this sample app uses npm's workspaces, we recommend running the npm commands from the root of the repo. +**Run from `/javascript` root**: since this sample app uses npm's workspaces, we recommend running the npm commands from the root of the `/javascript` folder. ```sh -# Install all dependencies (no need to pass the -w option) +# Install all dependencies npm install ``` @@ -81,7 +69,7 @@ npm install Now, run the below commands to start the processes needed for building the application and running the servers for both client and API server: ```sh -# In one terminal window, run the following watch command from the root of the repository +# In one terminal window, run the following command from the `/javascript` directory npm run start:reactjs-todo-lw ``` diff --git a/javascript/reactjs-todo-login-widget/client/constants.js b/javascript/reactjs-todo-login-widget/client/constants.js index 0569fe15..10dd6af4 100755 --- a/javascript/reactjs-todo-login-widget/client/constants.js +++ b/javascript/reactjs-todo-login-widget/client/constants.js @@ -16,8 +16,6 @@ export const JOURNEY_LOGIN = process.env.JOURNEY_LOGIN; export const JOURNEY_REGISTER = process.env.JOURNEY_REGISTER; export const WEB_OAUTH_CLIENT = process.env.WEB_OAUTH_CLIENT; export const REALM_PATH = process.env.REALM_PATH; -export const CENTRALIZED_LOGIN = process.env.CENTRALIZED_LOGIN; export const SESSION_URL = `${SERVER_URL}json/realms/root/sessions`; -export const SERVER_TYPE = process.env.SERVER_TYPE; export const SCOPE = process.env.SCOPE; export const PINGONE_ENV_ID = process.env.PINGONE_ENV_ID; diff --git a/javascript/reactjs-todo-login-widget/client/context/README.md b/javascript/reactjs-todo-login-widget/client/context/README.md new file mode 100644 index 00000000..7a01fd20 --- /dev/null +++ b/javascript/reactjs-todo-login-widget/client/context/README.md @@ -0,0 +1,5 @@ +# React Context + +This application leverages "global state" with React's Context API. This can be useful to share state with any component without having to pass props through deeply nested components. Authentication status and theme state are good examples. + +If global state becomes a more complex function of the app, something like Redux might be a better option. \ No newline at end of file diff --git a/javascript/reactjs-todo-login-widget/client/context/auth.context.js b/javascript/reactjs-todo-login-widget/client/context/auth.context.js index af5fee20..9b097be0 100755 --- a/javascript/reactjs-todo-login-widget/client/context/auth.context.js +++ b/javascript/reactjs-todo-login-widget/client/context/auth.context.js @@ -68,14 +68,8 @@ export function useInitAuthState() { ********************************************************************* */ if (DEBUGGER) debugger; try { - if (process.env.SERVER_TYPE === 'PINGONE') { - await user.logout({ - logoutRedirectUri: `${window.location.origin}`, - }); - } else { - await user.logout(); - location.assign(`${document.location.origin}/`); - } + await user.logout(); + location.assign(`${document.location.origin}/`); } catch (err) { console.error(`Error: logout did not successfully complete; ${err}`); } diff --git a/javascript/reactjs-todo-login-widget/playwright.config.ts b/javascript/reactjs-todo-login-widget/playwright.config.ts index 38ef7cf5..6ba5ee51 100644 --- a/javascript/reactjs-todo-login-widget/playwright.config.ts +++ b/javascript/reactjs-todo-login-widget/playwright.config.ts @@ -17,27 +17,39 @@ export default defineConfig({ }, webServer: [ { - command: 'npm run start:reactjs-todo-lw', + command: 'npm run start', url, timeout: 120 * 1000, reuseExistingServer: !process.env.CI, - cwd: '../', + cwd: './', env: { API_URL: 'http://localhost:9443', DEBUGGER_OFF: 'true', DEVELOPMENT: 'false', JOURNEY_LOGIN: 'Login', JOURNEY_REGISTER: 'Registration', - PORT: '9443', + PORT: '8443', SERVER_URL: 'https://openam-sdks.forgeblocks.com/am', REALM_PATH: 'alpha', SCOPE: 'profile me.read email', - TIMEOUT: '3000', WEB_OAUTH_CLIENT: 'CentralLoginOAuthClient-', + PINGONE_ENV_ID: '02fb4743-189a-4bc7-9d6c-a919edfe6447', + }, + ignoreHTTPSErrors: true, + }, + { + command: 'npm run start', + url: 'http://localhost:9443/healthcheck', + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI, + cwd: '../todo-api/', + env: { + PORT: '9443', + SERVER_TYPE: 'AIC', + SERVER_URL: 'https://openam-sdks.forgeblocks.com/am', + REALM_PATH: 'alpha', REST_OAUTH_CLIENT: 'RestOAuthClient', REST_OAUTH_SECRET: process.env.REST_OAUTH_SECRET || '', - CENTRALIZED_LOGIN: 'false', - PINGONE_ENV_ID: '02fb4743-189a-4bc7-9d6c-a919edfe6447', }, ignoreHTTPSErrors: true, }, diff --git a/javascript/reactjs-todo-login-widget/webpack.config.js b/javascript/reactjs-todo-login-widget/webpack.config.js index bcf78ae9..07c37246 100644 --- a/javascript/reactjs-todo-login-widget/webpack.config.js +++ b/javascript/reactjs-todo-login-widget/webpack.config.js @@ -18,6 +18,7 @@ module.exports = () => { const localEnv = dotenv.config().parsed || {}; // Use process environment variables for prod, but fallback to local .env for dev + const PORT = process.env.PORT || localEnv.PORT || '8443'; const SERVER_URL = process.env.SERVER_URL || localEnv.SERVER_URL; const APP_URL = process.env.APP_URL || localEnv.APP_URL; const API_URL = process.env.API_URL || localEnv.API_URL; @@ -26,10 +27,8 @@ module.exports = () => { const JOURNEY_LOGIN = process.env.JOURNEY_LOGIN || localEnv.JOURNEY_LOGIN; const JOURNEY_REGISTER = process.env.JOURNEY_REGISTER || localEnv.JOURNEY_REGISTER; const WEB_OAUTH_CLIENT = process.env.WEB_OAUTH_CLIENT || localEnv.WEB_OAUTH_CLIENT; - const CENTRALIZED_LOGIN = process.env.CENTRALIZED_LOGIN || localEnv.CENTRALIZED_LOGIN; const REALM_PATH = process.env.REALM_PATH || localEnv.REALM_PATH; const SCOPE = process.env.SCOPE || localEnv.SCOPE; - const SERVER_TYPE = process.env.SERVER_TYPE || localEnv.SERVER_TYPE; const PINGONE_ENV_ID = process.env.PINGONE_ENV_ID || localEnv.PINGONE_ENV_ID; return { @@ -108,7 +107,7 @@ module.exports = () => { client: { overlay: false, }, - port: 8443, + port: PORT, historyApiFallback: true, }, plugins: [ @@ -122,10 +121,8 @@ module.exports = () => { 'process.env.JOURNEY_LOGIN': JSON.stringify(JOURNEY_LOGIN), 'process.env.JOURNEY_REGISTER': JSON.stringify(JOURNEY_REGISTER), 'process.env.WEB_OAUTH_CLIENT': JSON.stringify(WEB_OAUTH_CLIENT), - 'process.env.CENTRALIZED_LOGIN': JSON.stringify(CENTRALIZED_LOGIN), 'process.env.REALM_PATH': JSON.stringify(REALM_PATH), 'process.env.SCOPE': JSON.stringify(SCOPE), - 'process.env.SERVER_TYPE': JSON.stringify(SERVER_TYPE), 'process.env.PINGONE_ENV_ID': JSON.stringify(PINGONE_ENV_ID), }), ], diff --git a/javascript/reactjs-todo-oidc/.env.example b/javascript/reactjs-todo-oidc/.env.example index 99860400..3aaf07cc 100644 --- a/javascript/reactjs-todo-oidc/.env.example +++ b/javascript/reactjs-todo-oidc/.env.example @@ -1,10 +1,9 @@ -API_URL= -DEBUGGER_OFF=true -DEVELOPMENT= -REALM_PATH=alpha WEB_OAUTH_CLIENT= -SCOPE= WELLKNOWN_URL= -INIT_PROTECT= -# required when PingOne Protect callbacks are present -PINGONE_ENV_ID= +SCOPE= +#SERVER - 'PINGAM' or 'PINGONE' +SERVER= +API_URL=http://localhost:9443 +PORT=8443 +DEBUGGER_OFF=true +DEVELOPMENT=true diff --git a/javascript/reactjs-todo-oidc/README.md b/javascript/reactjs-todo-oidc/README.md index 28620433..bda023ce 100644 --- a/javascript/reactjs-todo-oidc/README.md +++ b/javascript/reactjs-todo-oidc/README.md @@ -6,35 +6,50 @@ This sample code is provided "as is" and is not a supported product of Ping. Its ## Requirements -1. An instance of Ping's Access Manager (AM), either within Ping Advanced Identity Cloud, your own private installation, or locally installed on your computer +1. A PingOne tenant or instance of Ping's Access Manager (PingAM), either within Ping Advanced Identity Cloud, your own private installation, or locally installed on your computer 2. Node >= 18 (recommended: install via the [official package installer](https://nodejs.org/en/)) 3. Knowledge of using the Terminal/Command Line -4. Ability to generate security certs (recommended: mkcert; [installation instructions here](https://github.com/FiloSottile/mkcert#installation)) -5. This project cloned to your computer +4. This project cloned to your computer ## Setup -Once you have the 5 requirements above met, we can build the project. +Once you have the requirements above met, we can build the project. -### Set Up Your AM Instance +### Set Up A Public Client +Choose to set up either a PingOne or PingAM/PingAIC instance -#### Configure CORS +#### Set Up Your AM Instance + +##### Configure CORS 1. Allowed origins: `https://localhost:8443` 2. Allowed methods: `GET` `POST` 3. Allowed headers: `Content-Type` `X-Requested-With` `X-Requested-Platform` `Accept-API-Version` `Authorization` 4. Allow credentials: enable -#### Create Your OAuth Clients +##### Create Your OAuth Clients 1. Create a public (SPA) OAuth client for the web app: no secret, scopes including `openid profile email`, implicit consent enabled, and no "token authentication endpoint method". - Redirect URI: `https://localhost:8443/callback.html` - Post logout redirect URI: `https://localhost:8443/` 2. Create a confidential (Node.js) OAuth client for the API server: with a secret, default scope of `am-introspect-all-tokens`, and `client_secret_basic` as the "token authentication endpoint method". -#### Configure OIDC Discovery +#### Setup Your PingOne application + +1. Create a new OIDC Web App + +##### Configuration -Set your `.env` values to point to your realm-specific OpenID configuration endpoint (`WELLKNOWN_URL`) and keep `SCOPE` configured with at least `openid`. +1. CORS Allowed origins: `https://localhost:8443` +2. Token Auth Method: None +3. Signoff URLs: https://localhost:8443/logout +4. Redirect URIs: https://localhost:8443/callback.html +5. Response Type: Code +6. Grant Type: Authorization Code + +##### Resources (scopes) + +1. openid profile email phone name revoke ### Configure Your `.env` File @@ -43,23 +58,22 @@ Change the name of `.env.example` to `.env` and replace the bracketed values (e. Example with annotations: ```text +WEB_OAUTH_CLIENT=<<>> +SCOPE="openid profile email" +WELLKNOWN_URL=<<>> +SERVER=PINGAM API_URL=http://localhost:9443 +PORT=8443 DEBUGGER_OFF=true DEVELOPMENT=true -REALM_PATH=<<>> -WEB_OAUTH_CLIENT=<<>> -SCOPE="openid profile email" -WELLKNOWN_URL=<<>> -INIT_PROTECT=bootstrap -PINGONE_ENV_ID=<<>> ``` ### Installing Dependencies and Run Build -Run commands from the JavaScript workspace root: +**Run from root of `/javascript`**: since this sample app uses npm's workspaces, we recommend running the npm commands from the root of the `/javascript` folder. ```sh -cd javascript +# Install all dependencies npm install ``` @@ -68,15 +82,11 @@ npm install Run the command below to start both the client app and `todo-api`: ```sh -cd javascript +# In a terminal window, run the following command from the `/javascript` folder npm run start:reactjs-todo-oidc ``` -Now, you should be able to visit `https://localhost:8443`, which is your web app or client (the Relying Party in OAuth terms). This client will make requests to your AM instance (the Authorization Server in OAuth terms), and `http://localhost:9443` as the REST API for your todos (the Resource Server). - -### Accept Cert Exceptions - -You will likely have to accept security certificate exceptions for both your React app and the Node.js server. To accept the cert from the Node.js server, you can visit `http://localhost:9443/healthcheck` in your browser. Once you receive `OK`, your Node.js server is running on the correct domain and port, and the cert is accepted. +Now, you should be able to visit `https://localhost:8443`, which is your web app or client (the Relying Party in OAuth terms). This client will make requests to PingAM or PingOne (the Authorization Server in OAuth terms), and `http://localhost:9443` as the REST API for your todos (the Resource Server). ## Learn About Integration Touchpoints diff --git a/javascript/reactjs-todo-oidc/client/README.md b/javascript/reactjs-todo-oidc/client/README.md index c24f2e6a..dcae25d6 100644 --- a/javascript/reactjs-todo-oidc/client/README.md +++ b/javascript/reactjs-todo-oidc/client/README.md @@ -1,7 +1,6 @@ # React JS Todo OIDC Client -This folder contains the React client for `reactjs-todo-oidc`, which demonstrates centralized OIDC login with `@forgerock/oidc-client`. -Unlike journey-based samples, this client does not render embedded login callbacks and does not expose a `/register` route. +This folder contains the React client for `reactjs-todo-oidc`, which demonstrates centralized OIDC login with `@forgerock/oidc-client`. Unlike journey-based samples, this client does not render embedded login callbacks and does not expose a `/register` route. ## Authentication flow @@ -16,24 +15,19 @@ Unlike journey-based samples, this client does not render embedded login callbac Copy `.env.example` to `.env` at `javascript/reactjs-todo-oidc/.env`, then set values for your environment: ```text -SERVER_URL=<<>> -WELLKNOWN_URL=<<>> -REALM_PATH=<<>> +WELLKNOWN_URL=<<>> WEB_OAUTH_CLIENT=<<>> SCOPE="openid profile email" API_URL=http://localhost:9443 -REST_OAUTH_CLIENT=<<>> -REST_OAUTH_SECRET=<<>> DEBUGGER_OFF=true -INIT_PROTECT=bootstrap -PINGONE_ENV_ID=<<>> +SERVER=PINGAM ``` Notes: - `WELLKNOWN_URL` is the source of truth for OIDC discovery in this sample. - `SCOPE` should include `openid` so logout and userinfo flows have the expected token set. -- `SERVER_URL` is kept in `.env` for parity with sample conventions, though OIDC discovery uses `WELLKNOWN_URL`. +- `SERVER` is used to derive the user name from either PingAM or PingOne ## Running locally diff --git a/javascript/reactjs-todo-oidc/client/constants.js b/javascript/reactjs-todo-oidc/client/constants.js index 9e3e5aa6..82920861 100755 --- a/javascript/reactjs-todo-oidc/client/constants.js +++ b/javascript/reactjs-todo-oidc/client/constants.js @@ -12,11 +12,9 @@ export const API_URL = process.env.API_URL; // Yes, the debugger boolean is intentionally reversed export const DEBUGGER = process.env.DEBUGGER_OFF === 'false'; export const WEB_OAUTH_CLIENT = process.env.WEB_OAUTH_CLIENT; -export const REALM_PATH = process.env.REALM_PATH; export const SCOPE = process.env.SCOPE; export const WELLKNOWN_URL = process.env.WELLKNOWN_URL; -export const INIT_PROTECT = process.env.INIT_PROTECT; -export const PINGONE_ENV_ID = process.env.PINGONE_ENV_ID; +export const SERVER = process.env.SERVER; export const CONFIG = { clientId: WEB_OAUTH_CLIENT, @@ -25,5 +23,4 @@ export const CONFIG = { serverConfig: { wellknown: WELLKNOWN_URL, }, - realmPath: REALM_PATH, }; diff --git a/javascript/reactjs-todo-oidc/client/context/README.md b/javascript/reactjs-todo-oidc/client/context/README.md new file mode 100644 index 00000000..7a01fd20 --- /dev/null +++ b/javascript/reactjs-todo-oidc/client/context/README.md @@ -0,0 +1,5 @@ +# React Context + +This application leverages "global state" with React's Context API. This can be useful to share state with any component without having to pass props through deeply nested components. Authentication status and theme state are good examples. + +If global state becomes a more complex function of the app, something like Redux might be a better option. \ No newline at end of file diff --git a/javascript/reactjs-todo-oidc/client/context/protect.context.js b/javascript/reactjs-todo-oidc/client/context/protect.context.js deleted file mode 100644 index 583a5355..00000000 --- a/javascript/reactjs-todo-oidc/client/context/protect.context.js +++ /dev/null @@ -1,69 +0,0 @@ -/* - * ping-sample-web-react-todo-oidc - * - * protect.context.js - * - * Copyright (c) 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. - */ -import { protect } from '@forgerock/protect'; -import { createContext, useState, useEffect } from 'react'; -import { DEBUGGER, INIT_PROTECT } from '../constants'; - -const urlParams = new URLSearchParams(window.location.search); -const protectInitMode = INIT_PROTECT || urlParams.get('initProtect'); - -/** - * @function useInitProtectApi - A custom hook to get a bootstrapped Protect API - * @returns {Array} - A Protect API if opting for bootstrap initialization and a setter method - */ -export function useInitProtect(config) { - /** - * Create state properties for "global" Protect API. - * The destructuring of the hook's array results in index 0 having the Protect API, - * and index 1 having the "setter" method to set a new API. - */ - const [protectApi, setProtectApi] = useState(null); - - useEffect(() => { - async function bootstrapProtect() { - /** ************************************************************************* - * SDK INTEGRATION POINT - * Summary: Initialize Protect at bootstrap - * -------------------------------------------------------------------------- - * Details: If the INIT_PROTECT flag is set to 'bootstrap', then initialize - * PingOne Protect as early as possible in the application for data collection. - * Call the useInitProtect hook as early as possible in your application. - * The PingOne environment ID is required while all other options in the - * configuration are optional. - ************************************************************************* */ - if (DEBUGGER) debugger; - const api = protect(config); - const result = await api.start(); - if (result?.error) { - console.error('Failed to initialize Protect at bootstrap'); - } else { - console.log('Protect initialized at bootstrap for data collection'); - } - - setProtectApi(api); - } - - if (protectInitMode === 'bootstrap' && !protectApi) { - bootstrapProtect(); - } - }, [protectApi]); - - /** - * Returns an array with Protect API or null at index zero and setter at index one - */ - return [protectApi, setProtectApi]; -} - -/** - * @constant ProtectContext - Creates React Context to store Protect API - * This provides the capability to set a global Protect API state in React - * without having to pass the state as props through parent-child components. - */ -export const ProtectContext = createContext(null); diff --git a/javascript/reactjs-todo-oidc/client/index.js b/javascript/reactjs-todo-oidc/client/index.js index b922ee1d..0162c4fd 100755 --- a/javascript/reactjs-todo-oidc/client/index.js +++ b/javascript/reactjs-todo-oidc/client/index.js @@ -13,10 +13,9 @@ import ReactDOM from 'react-dom/client'; import Loading from './components/utilities/loading'; import { OidcContext, useInitOidcState } from './context/oidc.context'; -import { useInitProtect, ProtectContext } from './context/protect.context'; import { ThemeContext, initTheme } from './context/theme.context'; import Router from './router'; -import { DEBUGGER, INIT_PROTECT, PINGONE_ENV_ID } from './constants'; +import { DEBUGGER } from './constants'; /** * This import will produce a separate CSS file linked in the index.html @@ -24,10 +23,6 @@ import { DEBUGGER, INIT_PROTECT, PINGONE_ENV_ID } from './constants'; */ import './styles/index.scss'; -if (DEBUGGER) debugger; -const urlParams = new URLSearchParams(window.location.search); -const protectInitMode = INIT_PROTECT || urlParams.get('initProtect'); - /** * Initialize the React application */ @@ -48,13 +43,12 @@ const protectInitMode = INIT_PROTECT || urlParams.get('initProtect'); * If global state becomes a more complex function of the app, * something like Redux might be a better option. */ + if (DEBUGGER) debugger; const theme = initTheme(); const oidcState = useInitOidcState(); - const protectState = useInitProtect({ envId: PINGONE_ENV_ID }); const [{ oidcClient }] = oidcState; - const [protectApi] = protectState; - if (!oidcClient || (protectInitMode === 'bootstrap' && !protectApi)) { + if (!oidcClient) { return ( @@ -67,9 +61,7 @@ const protectInitMode = INIT_PROTECT || urlParams.get('initProtect'); return ( - - - + ); diff --git a/javascript/reactjs-todo-oidc/client/views/home.js b/javascript/reactjs-todo-oidc/client/views/home.js index 22b3c476..540dd2f7 100755 --- a/javascript/reactjs-todo-oidc/client/views/home.js +++ b/javascript/reactjs-todo-oidc/client/views/home.js @@ -57,7 +57,7 @@ export default function Home() { library and our{' '} JavaScript SDK @@ -72,7 +72,9 @@ export default function Home() { this project can be found on Github {' '} and run locally for experimentation. For more on our SDKs,{' '} - you can find our official SDK documentation here. + + you can find our official SDK documentation here. +

); diff --git a/javascript/reactjs-todo-oidc/client/views/login.js b/javascript/reactjs-todo-oidc/client/views/login.js index 96d603c5..8e189cba 100755 --- a/javascript/reactjs-todo-oidc/client/views/login.js +++ b/javascript/reactjs-todo-oidc/client/views/login.js @@ -7,12 +7,13 @@ * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ -import React, { useContext, useEffect, useState } from 'react'; +import React, { useContext, useEffect, useState, useCallback } from 'react'; import { useSearchParams, useNavigate } from 'react-router-dom'; import BackHome from '../components/utilities/back-home'; import Loading from '../components/utilities/loading'; import Card from '../components/layout/card'; +import { SERVER, DEBUGGER } from '../constants'; import { OidcContext } from '../context/oidc.context'; /** @@ -27,63 +28,76 @@ export default function Login() { const codeParam = params.get('code'); const errorParam = params.get('error'); const stateParam = params.get('state'); - const [state, setState] = useState({ - loadingMessage: '', - }); + const [loadingMessage, setLoadingMessage] = useState(''); - useEffect(() => { - async function checkCentralizedLogin() { - try { - if (!oidcClient) { - setState({ - loadingMessage: 'Initializing application...', - }); - return; - } + const authorize = useCallback( + async function authorizeCallback(codeParam, stateParam) { + /** ***************************************************************** + * SDK INTEGRATION POINT + * Summary: Get OAuth/OIDC tokens and user info + * ------------------------------------------------------------------ + * Details: Exchange code and state for tokens. With valid tokens + * stored in storage, we can then request user info and set app state + ***************************************************************** */ + if (DEBUGGER) debugger; - if (codeParam && stateParam) { - const tokens = await oidcClient.token.exchange(codeParam, stateParam); - if ('error' in tokens) { - setState({ - loadingMessage: 'Sign in failed. Please try again.', - }); - console.error(`Error: token exchange; ${tokens.error}`); - return; - } + const tokens = await oidcClient.token.exchange(codeParam, stateParam); + if ('error' in tokens) { + setLoadingMessage('Sign in failed. Please try again.'); + console.error(`Error: token exchange; ${tokens.error}`); + return; + } - const user = await oidcClient.user.info(); - if ('error' in user) { - setState({ - loadingMessage: 'Sign in failed. Please try again.', - }); - console.error(`Error: get current user; ${user.error}`); - return; - } + const user = await oidcClient.user.info(); + if ('error' in user) { + setLoadingMessage('Sign in failed. Please try again.'); + console.error(`Error: get current user; ${user.error}`); + return; + } + + const username = + SERVER === 'PINGONE' ? `${user.given_name ?? ''} ${user.family_name ?? ''}` : user.name; - setState({ - loadingMessage: 'Success! Redirecting ...', - }); + methods.setUser(username); + methods.setEmail(user.email); + methods.setAuthentication(true); + navigate('/'); + }, + [oidcClient, methods, navigate], + ); - methods.setUser(user.name); - methods.setEmail(user.email); - methods.setAuthentication(true); - navigate('/'); + useEffect(() => { + async function handleCentralizedLogin() { + try { + if (codeParam && stateParam) { + /** + * When the user returns to this app after successfully logging in, + * the URL will include code and state query parameters that need to + * be passed in to complete the OAuth flow giving the user access + */ + setLoadingMessage('Success! Redirecting ...'); + await authorize(codeParam, stateParam); } else if (errorParam) { - setState({ - loadingMessage: 'Sign in failed. Please try again.', - }); + setLoadingMessage('Sign in failed. Please try again.'); } else { - setState({ - loadingMessage: 'Redirecting ...', - }); + /** ***************************************************************** + * SDK INTEGRATION POINT + * Summary: Redirect the user to the authorization URL + * ------------------------------------------------------------------ + * Details: Use the OIDC client to get an authorization URL to redirect + * the user to sign in. + ***************************************************************** */ + if (DEBUGGER) debugger; + + setLoadingMessage('Redirecting ...'); + const authorizeUrl = await oidcClient.authorize.url(); if (typeof authorizeUrl !== 'string' && 'error' in authorizeUrl) { - setState({ - loadingMessage: 'Sign in failed. Please try again.', - }); + setLoadingMessage('Sign in failed. Please try again.'); console.error(`Error: centralized login; ${authorizeUrl.error}`); return; } + window.location.assign(authorizeUrl); } } catch (error) { @@ -91,15 +105,15 @@ export default function Login() { } } - checkCentralizedLogin(); - }, [oidcClient]); + handleCentralizedLogin(); + }, [authorize, codeParam, errorParam, methods, navigate, oidcClient, stateParam]); return (
- +
diff --git a/javascript/reactjs-todo-oidc/e2e/oidc-login.spec.js b/javascript/reactjs-todo-oidc/e2e/oidc-login-pingam.spec.js similarity index 96% rename from javascript/reactjs-todo-oidc/e2e/oidc-login.spec.js rename to javascript/reactjs-todo-oidc/e2e/oidc-login-pingam.spec.js index 04d53f59..2251bbb6 100644 --- a/javascript/reactjs-todo-oidc/e2e/oidc-login.spec.js +++ b/javascript/reactjs-todo-oidc/e2e/oidc-login-pingam.spec.js @@ -1,7 +1,7 @@ /* * ping-sample-web-react-todo-oidc * - * oidc-login.spec.js + * oidc-login-pingam.spec.js * * Copyright (c) 2026 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms @@ -9,7 +9,7 @@ * */ import { test, expect } from '@playwright/test'; -import { password, username } from './utils/demo-user'; +import { amPassword, amUsername } from './utils/demo-user'; const BASE_URL = 'https://localhost:8443'; @@ -26,7 +26,7 @@ function getConfiguredOidcHost() { return new URL(configuredWellKnownUrl).hostname; } -async function submitLoginForm(page, testUsername = username, testPassword = password) { +async function submitLoginForm(page, testUsername = amUsername, testPassword = amPassword) { const usernameField = page.getByLabel('Username'); const userNameField = page.getByLabel('User Name'); diff --git a/javascript/reactjs-todo-oidc/e2e/oidc-login-pingone.spec.js b/javascript/reactjs-todo-oidc/e2e/oidc-login-pingone.spec.js new file mode 100644 index 00000000..5f1ced65 --- /dev/null +++ b/javascript/reactjs-todo-oidc/e2e/oidc-login-pingone.spec.js @@ -0,0 +1,85 @@ +/* + * ping-sample-web-react-todo-oidc + * + * oidc-login-pingone.spec.js + * + * Copyright (c) 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. + * + */ +import { test, expect } from '@playwright/test'; +import { p1DisplayName, p1Password, p1Username } from './utils/demo-user'; + +const BASE_URL = 'https://localhost:8444'; + +test.describe('React - PingOne OIDC', () => { + test('Starts centralized login flow, pass', async ({ page }) => { + await page.goto(BASE_URL); + await page.getByRole('link', { name: 'Sign In', exact: true }).click(); + await expect(page.getByRole('heading', { name: 'Sign On' })).toBeVisible(); + await expect(page.getByRole('textbox', { name: 'Username' })).toBeVisible(); + await expect(page.getByRole('textbox', { name: 'Password' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Sign On' })).toBeVisible(); + + const authorizeUrl = new URL(page.url()); + expect(authorizeUrl.searchParams.get('client_id')).toBe('724ec718-c41c-4d51-98b0-84a583f450f9'); + expect(authorizeUrl.searchParams.get('redirect_uri')).toContain(`${BASE_URL}/callback.html`); + expect(authorizeUrl.searchParams.get('state')).toBeTruthy(); + expect(authorizeUrl.searchParams.get('code_challenge')).toBeTruthy(); + }); + + test('Login with valid credentials, pass', async ({ page }) => { + await page.goto(BASE_URL); + await page.getByRole('link', { name: 'Sign In', exact: true }).click(); + + await expect(page.getByRole('heading', { name: 'Sign On' })).toBeVisible(); + await page.getByRole('textbox', { name: 'Username' }).fill(p1Username); + await page.getByRole('textbox', { name: 'Password' }).fill(p1Password); + await page.getByRole('button', { name: 'Sign On' }).click(); + + await expect(page.getByText(`Welcome back, ${p1DisplayName}!`)).toBeVisible(); + await expect(page.getByRole('link', { name: 'Sign In', exact: true })).not.toBeVisible(); + await expect(page.getByText('Protect with Ping')).toBeVisible(); + }); + + test('Login with invalid credentials, fail', async ({ page }) => { + await page.goto(BASE_URL); + await page.getByRole('link', { name: 'Sign In', exact: true }).click(); + + await expect(page.getByRole('heading', { name: 'Sign On' })).toBeVisible(); + await page.getByRole('textbox', { name: 'Username' }).fill('invalidUsername'); + await page.getByRole('textbox', { name: 'Password' }).fill('invalidPassword'); + await page.getByRole('button', { name: 'Sign On' }).click(); + + await expect( + page.getByText(/Invalid username and\/or password|Validation Error/), + ).toBeVisible(); + }); + + test('Login then logout, pass', async ({ page }) => { + // Login + await page.goto(BASE_URL); + await page.getByRole('link', { name: 'Sign In', exact: true }).click(); + + await expect(page.getByRole('heading', { name: 'Sign On' })).toBeVisible(); + await page.getByRole('textbox', { name: 'Username' }).fill(p1Username); + await page.getByRole('textbox', { name: 'Password' }).fill(p1Password); + await page.getByRole('button', { name: 'Sign On' }).click(); + + await expect(page.getByText(`Welcome back, ${p1DisplayName}!`)).toBeVisible(); + await expect(page.getByRole('link', { name: 'Sign In', exact: true })).not.toBeVisible(); + await expect(page.getByText('Protect with Ping')).toBeVisible(); + + // Logout + await page.locator('#account_dropdown').click(); + await page.getByRole('link', { name: 'Sign Out' }).click(); + + await page.waitForURL(BASE_URL + '/logout'); + await page.waitForURL(BASE_URL); + + await expect(page.getByText('Welcome back')).not.toBeVisible(); + await expect(page.getByText('Protect with Ping')).toBeVisible(); + await expect(page.getByRole('link', { name: 'Sign In', exact: true })).toBeVisible(); + }); +}); diff --git a/javascript/reactjs-todo-oidc/e2e/oidc-todo.spec.js b/javascript/reactjs-todo-oidc/e2e/oidc-todo.spec.js index 01ed88f3..f082e3c2 100644 --- a/javascript/reactjs-todo-oidc/e2e/oidc-todo.spec.js +++ b/javascript/reactjs-todo-oidc/e2e/oidc-todo.spec.js @@ -10,7 +10,7 @@ */ import { test, expect } from '@playwright/test'; import { asyncEvents } from './utils/async-events'; -import { username, password } from './utils/demo-user'; +import { amUsername, amPassword } from './utils/demo-user'; const BASE_URL = 'https://localhost:8443'; @@ -25,8 +25,8 @@ test.describe.serial('React - OIDC Todo', () => { await page.goto(BASE_URL); await clickLink('Sign In', 'https://openam-sdks.forgeblocks.com/'); - await page.getByLabel('User Name').fill(username); - await page.getByRole('textbox', { name: 'Password' }).fill(password); + await page.getByLabel('User Name').fill(amUsername); + await page.getByRole('textbox', { name: 'Password' }).fill(amPassword); await page.getByRole('button', { name: 'Next' }).click(); await page.getByRole('link', { name: 'Todos', exact: true }).click(); diff --git a/javascript/reactjs-todo-oidc/e2e/utils/demo-user.js b/javascript/reactjs-todo-oidc/e2e/utils/demo-user.js index 4c39debd..1540561f 100644 --- a/javascript/reactjs-todo-oidc/e2e/utils/demo-user.js +++ b/javascript/reactjs-todo-oidc/e2e/utils/demo-user.js @@ -5,6 +5,10 @@ * of the MIT license. See the LICENSE file for details. * */ -export const username = 'JsAmSampleAppsE2E@user.com'; -export const password = 'Demo_12345!'; -export const displayName = 'Demo User'; +export const amUsername = 'JsAmSampleAppsE2E@user.com'; +export const amPassword = 'Demo_12345!'; +export const amDisplayName = 'Demo User'; + +export const p1Username = 'JsDvSampleAppsE2E@user.com'; +export const p1Password = 'Demo_12345!'; +export const p1DisplayName = 'JS DaVinci Sample Apps E2E'; diff --git a/javascript/reactjs-todo-oidc/playwright.config.ts b/javascript/reactjs-todo-oidc/playwright.config.ts index 9c91a734..e7186e8b 100644 --- a/javascript/reactjs-todo-oidc/playwright.config.ts +++ b/javascript/reactjs-todo-oidc/playwright.config.ts @@ -31,17 +31,33 @@ export default defineConfig({ DEBUGGER_OFF: 'true', DEVELOPMENT: 'false', PORT: '8443', - SERVER_URL: 'https://openam-sdks.forgeblocks.com/am', + SERVER: 'PINGAM', WELLKNOWN_URL: 'https://openam-sdks.forgeblocks.com/am/oauth2/alpha/.well-known/openid-configuration', - REALM_PATH: 'alpha', SCOPE: 'openid profile me.read email', - TIMEOUT: '3000', WEB_OAUTH_CLIENT: 'CentralLoginOAuthClient-', REST_OAUTH_CLIENT: 'RestOAuthClient', REST_OAUTH_SECRET: process.env.REST_OAUTH_SECRET || '', - INIT_PROTECT: 'bootstrap', - PINGONE_ENV_ID: '02fb4743-189a-4bc7-9d6c-a919edfe6447', + }, + ignoreHTTPSErrors: true, + }, + { + command: 'npm run start', + url: 'https://localhost:8444', + timeout: 120 * 1000, + reuseExistingServer: false, + cwd: './', + env: { + API_URL: 'http://localhost:9443', + DEBUGGER_OFF: 'true', + DEVELOPMENT: 'false', + PORT: '8444', + SERVER: 'PINGONE', + WELLKNOWN_URL: + 'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration', + SCOPE: 'openid profile email revoke', + WEB_OAUTH_CLIENT: '724ec718-c41c-4d51-98b0-84a583f450f9', + REST_OAUTH_CLIENT: '724ec718-c41c-4d51-98b0-84a583f450f9', }, ignoreHTTPSErrors: true, }, diff --git a/javascript/reactjs-todo-oidc/webpack.config.js b/javascript/reactjs-todo-oidc/webpack.config.js index aeb9060c..50ed7fda 100644 --- a/javascript/reactjs-todo-oidc/webpack.config.js +++ b/javascript/reactjs-todo-oidc/webpack.config.js @@ -18,18 +18,14 @@ module.exports = () => { const localEnv = dotenv.config().parsed || {}; // Use process environment variables for prod, but fallback to local .env for dev - const SERVER_URL = process.env.SERVER_URL || localEnv.SERVER_URL; - const APP_URL = process.env.APP_URL || localEnv.APP_URL; + const PORT = process.env.PORT || localEnv.PORT || '8443'; const API_URL = process.env.API_URL || localEnv.API_URL; const DEBUGGER_OFF = process.env.DEBUGGER_OFF || localEnv.DEBUGGER_OFF; const DEVELOPMENT = process.env.DEVELOPMENT || localEnv.DEVELOPMENT; const WEB_OAUTH_CLIENT = process.env.WEB_OAUTH_CLIENT || localEnv.WEB_OAUTH_CLIENT; - const REALM_PATH = process.env.REALM_PATH || localEnv.REALM_PATH; const SCOPE = process.env.SCOPE || localEnv.SCOPE; - const SERVER_TYPE = process.env.SERVER_TYPE || localEnv.SERVER_TYPE; const WELLKNOWN_URL = process.env.WELLKNOWN_URL || localEnv.WELLKNOWN_URL; - const INIT_PROTECT = process.env.INIT_PROTECT || localEnv.INIT_PROTECT; - const PINGONE_ENV_ID = process.env.PINGONE_ENV_ID || localEnv.PINGONE_ENV_ID; + const SERVER = process.env.SERVER || localEnv.SERVER; return { // Point to the top level source file @@ -107,24 +103,19 @@ module.exports = () => { client: { overlay: false, }, - port: 8443, + port: PORT, historyApiFallback: true, }, plugins: [ new MiniCssExtractPlugin(), new webpack.DefinePlugin({ // Inject all the environment variable into the Webpack build - 'process.env.SERVER_URL': JSON.stringify(SERVER_URL), - 'process.env.APP_URL': JSON.stringify(APP_URL), 'process.env.API_URL': JSON.stringify(API_URL), 'process.env.DEBUGGER_OFF': JSON.stringify(DEBUGGER_OFF), 'process.env.WEB_OAUTH_CLIENT': JSON.stringify(WEB_OAUTH_CLIENT), - 'process.env.REALM_PATH': JSON.stringify(REALM_PATH), 'process.env.SCOPE': JSON.stringify(SCOPE), - 'process.env.SERVER_TYPE': JSON.stringify(SERVER_TYPE), 'process.env.WELLKNOWN_URL': JSON.stringify(WELLKNOWN_URL), - 'process.env.INIT_PROTECT': JSON.stringify(INIT_PROTECT), - 'process.env.PINGONE_ENV_ID': JSON.stringify(PINGONE_ENV_ID), + 'process.env.SERVER': JSON.stringify(SERVER), }), ], }; From 5e7f1075eacf0d2d81088e020358cf4003b150d7 Mon Sep 17 00:00:00 2001 From: AJ Ancheta <7781450+ancheetah@users.noreply.github.com> Date: Wed, 29 Apr 2026 18:47:03 -0400 Subject: [PATCH 2/2] chore(reactjs-todo-journey): remove centralized login feature --- javascript/package-lock.json | 2 +- javascript/reactjs-todo-journey/.env.example | 2 - javascript/reactjs-todo-journey/README.md | 1 - .../reactjs-todo-journey/client/constants.js | 12 +- .../client/views/login.jsx | 149 +++--------------- .../e2e/centralized-login.spec.js | 31 ---- javascript/reactjs-todo-journey/package.json | 1 + .../reactjs-todo-journey/playwright.config.js | 1 - .../reactjs-todo-login-widget/.env.example | 1 - .../reactjs-todo-login-widget/README.md | 1 - .../client/README.md | 1 - .../webpack.config.js | 2 - .../reactjs-todo-oidc/client/views/login.js | 60 ++++--- .../e2e/oidc-login-pingone.spec.js | 4 +- javascript/reactjs-todo-oidc/package.json | 1 - 15 files changed, 53 insertions(+), 216 deletions(-) delete mode 100644 javascript/reactjs-todo-journey/e2e/centralized-login.spec.js diff --git a/javascript/package-lock.json b/javascript/package-lock.json index 84695415..801e6af6 100644 --- a/javascript/package-lock.json +++ b/javascript/package-lock.json @@ -30036,6 +30036,7 @@ "dependencies": { "@forgerock/journey-client": "latest", "@forgerock/oidc-client": "latest", + "@forgerock/protect": "latest", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.19.0" @@ -30609,7 +30610,6 @@ "license": "ISC", "dependencies": { "@forgerock/oidc-client": "latest", - "@forgerock/protect": "latest", "cookie-parser": "^1.4.5", "cors": "^2.8.5", "dotenv": "^10.0.0", diff --git a/javascript/reactjs-todo-journey/.env.example b/javascript/reactjs-todo-journey/.env.example index b8d3e61b..5e475352 100644 --- a/javascript/reactjs-todo-journey/.env.example +++ b/javascript/reactjs-todo-journey/.env.example @@ -1,4 +1,3 @@ -# VITE_APP_URL - not using this for preview-environment instead, we can use window.location.origin VITE_WELLKNOWN_URL= VITE_WEB_OAUTH_CLIENT= VITE_API_URL=http://localhost:9443 @@ -9,7 +8,6 @@ VITE_SCOPE='openid profile email' VITE_JOURNEY_LOGIN=Login VITE_JOURNEY_REGISTER=Registration VITE_REALM_PATH=alpha -VITE_CENTRALIZED_LOGIN=false # VITE_INIT_PROTECT (optional) - bootstrap | journey # 'bootstrap' will initialize protect at app bootstrap time diff --git a/javascript/reactjs-todo-journey/README.md b/javascript/reactjs-todo-journey/README.md index 1716e07b..cca04ea0 100644 --- a/javascript/reactjs-todo-journey/README.md +++ b/javascript/reactjs-todo-journey/README.md @@ -63,7 +63,6 @@ Change the name of `.env.example` to `.env` and fill the environment variables w Example with annotations: ```text -VITE_APP_URL=https://localhost:8443 # in develop we do not use this variable for dynamic deployment reasons VITE_WELLKNOWN_URL=<<>> VITE_WEB_OAUTH_CLIENT=<<>> VITE_API_URL=http://localhost:9443 diff --git a/javascript/reactjs-todo-journey/client/constants.js b/javascript/reactjs-todo-journey/client/constants.js index 4c90c4ae..94578d3d 100755 --- a/javascript/reactjs-todo-journey/client/constants.js +++ b/javascript/reactjs-todo-journey/client/constants.js @@ -8,9 +8,6 @@ * of the MIT license. See the LICENSE file for details. */ -const urlParams = new URLSearchParams(window.location.search); -const centralLoginParam = urlParams.get('centralLogin'); - export const API_URL = import.meta.env.VITE_API_URL; // Yes, the debugger boolean is intentionally reversed export const DEBUGGER = import.meta.env.VITE_DEBUGGER_OFF === 'false'; @@ -18,18 +15,11 @@ export const JOURNEY_LOGIN = import.meta.env.VITE_JOURNEY_LOGIN; export const JOURNEY_REGISTER = import.meta.env.VITE_JOURNEY_REGISTER; export const WEB_OAUTH_CLIENT = import.meta.env.VITE_WEB_OAUTH_CLIENT; export const REALM_PATH = import.meta.env.VITE_REALM_PATH; -export const CENTRALIZED_LOGIN = import.meta.env.VITE_CENTRALIZED_LOGIN; export const SCOPE = import.meta.env.VITE_SCOPE; export const WELLKNOWN_URL = import.meta.env.VITE_WELLKNOWN_URL; export const INIT_PROTECT = import.meta.env.VITE_INIT_PROTECT; export const PINGONE_ENV_ID = import.meta.env.VITE_PINGONE_ENV_ID; -const redirectUri = `${window.location.origin}/${ - CENTRALIZED_LOGIN === 'true' || centralLoginParam === 'true' - ? 'login?centralLogin=true' - : 'callback.html' -}`; - /** *************************************************************************** * SDK INTEGRATION POINT * Summary: Configure the SDK @@ -47,7 +37,7 @@ const redirectUri = `${window.location.origin}/${ *************************************************************************** */ export const CONFIG = { clientId: WEB_OAUTH_CLIENT, - redirectUri, + redirectUri: `${window.location.origin}/callback.html`, scope: SCOPE, serverConfig: { wellknown: WELLKNOWN_URL, diff --git a/javascript/reactjs-todo-journey/client/views/login.jsx b/javascript/reactjs-todo-journey/client/views/login.jsx index 42b89db7..fbb09dd6 100755 --- a/javascript/reactjs-todo-journey/client/views/login.jsx +++ b/javascript/reactjs-todo-journey/client/views/login.jsx @@ -7,14 +7,11 @@ * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ -import { useContext, useEffect, useState, useCallback } from 'react'; -import { Link, useSearchParams, useNavigate } from 'react-router-dom'; +import { useContext } from 'react'; +import { Link, useSearchParams } from 'react-router-dom'; import BackHome from '../components/utilities/back-home'; -import Loading from '../components/utilities/loading'; import Card from '../components/layout/card'; import Form from '../components/journey/form'; -import { CENTRALIZED_LOGIN, DEBUGGER } from '../constants'; -import { OidcContext } from '../context/oidc.context'; import { ThemeContext } from '../context/theme.context'; /** @@ -23,133 +20,25 @@ import { ThemeContext } from '../context/theme.context'; */ export default function Login() { const theme = useContext(ThemeContext); - - // Used for setting global authentication state - const [{ oidcClient }, methods] = useContext(OidcContext); - - // Used for redirection after success - const navigate = useNavigate(); const [params] = useSearchParams(); - - // Get the code and state from the URL query parameters - const codeParam = params.get('code'); - const errorParam = params.get('error'); - const stateParam = params.get('state'); - const centralLogin = params.get('centralLogin'); const journey = params.get('journey'); - // Get environment variable - const isCentralizedLogin = CENTRALIZED_LOGIN === 'true' || centralLogin === 'true'; - const [loadingMessage, setLoadingMessage] = useState(''); - - const authorize = useCallback( - async function authorizeCallback(codeParam, stateParam) { - /** ***************************************************************** - * SDK INTEGRATION POINT - * Summary: Get OAuth/OIDC tokens and user info - * ------------------------------------------------------------------ - * Details: Exchange code and state for tokens. With valid tokens - * stored in storage, we can then request user info and set app state - ***************************************************************** */ - if (DEBUGGER) debugger; - - const tokenResponse = await oidcClient.token.exchange(codeParam, stateParam); - if ('error' in tokenResponse) { - setLoadingMessage('Sign in failed. Please try again.'); - console.error('Token exchange error:', tokenResponse); - return; - } - - const user = await oidcClient.user.info(); - if ('error' in user) { - setLoadingMessage('Sign in failed. Please try again.'); - console.error('Error getting user:', user); - return; - } - - methods.setUser(user.name); - methods.setEmail(user.email); - methods.setAuthentication(true); - navigate('/'); - }, - [oidcClient, methods, navigate], - ); - - useEffect(() => { - async function checkCentralizedLogin() { - if (isCentralizedLogin) { - if (codeParam && stateParam) { - /** - * When the user returns to this app after successfully logging in, - * the URL will include code and state query parameters that need to - * be passed in to complete the OAuth flow giving the user access - */ - setLoadingMessage('Success! Redirecting ...'); - await authorize(codeParam, stateParam); - } else if (errorParam) { - setLoadingMessage('Sign in failed. Please try again.'); - } else { - /** ***************************************************************** - * SDK INTEGRATION POINT - * Summary: Redirect the user to the authorization URL - * ------------------------------------------------------------------ - * Details: Use the OIDC client to get an authorization URL to redirect - * the user to sign in. - ***************************************************************** */ - if (DEBUGGER) debugger; - - setLoadingMessage('Redirecting ...'); - - const authorizeUrl = await oidcClient.authorize.url(); - if (typeof authorizeUrl !== 'string' && 'error' in authorizeUrl) { - setLoadingMessage('Sign in failed. Please try again.'); - console.error('Authorization URL Error:', authorizeUrl); - return; - } - - window.location.assign(authorizeUrl); - } - } - } - checkCentralizedLogin(); - }, [ - authorize, - codeParam, - errorParam, - isCentralizedLogin, - oidcClient, - stateParam, - ]); - - if (!isCentralizedLogin) { - return ( -
-
- - -
- Don’t have an account? Sign up here! -

- } - journey={journey} - /> - -
-
- ); - } else { - return ( -
-
- - - - -
+ return ( +
+
+ + + + Don't have an account? Sign up here! +

+ } + journey={journey} + /> +
- ); - } +
+ ); } diff --git a/javascript/reactjs-todo-journey/e2e/centralized-login.spec.js b/javascript/reactjs-todo-journey/e2e/centralized-login.spec.js deleted file mode 100644 index 73ef6ee7..00000000 --- a/javascript/reactjs-todo-journey/e2e/centralized-login.spec.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * ping-sample-web-react-journey - * - * centralized-login.spec.js - * - * Copyright (c) 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. - */ - -import { test, expect } from '@playwright/test'; -import { password, username } from './utils/demo-user'; - -test('React Journey - Login with Centralized Login', async ({ page }) => { - await page.goto('http://localhost:8443/?centralLogin=true'); - - await page.getByRole('link', { name: 'Sign In', exact: true }).click(); - - await page.getByLabel('User Name').fill(username); - await page.getByLabel('Password').first().fill(password); - await page.getByRole('button', { name: 'Next' }).click(); - - // TODO: It should be fixed evantually. This line has been added as after succesfully logging in, the server - // stops responding and it is necessary to reload the page in order for it to work. FYI: Only happens when running - // e2e tests. - await page.waitForTimeout(3000); - - await page.reload(); - - await expect(page.getByText('Welcome back')).toBeVisible(); -}); diff --git a/javascript/reactjs-todo-journey/package.json b/javascript/reactjs-todo-journey/package.json index f60c3c5e..644cdf67 100644 --- a/javascript/reactjs-todo-journey/package.json +++ b/javascript/reactjs-todo-journey/package.json @@ -22,6 +22,7 @@ "dependencies": { "@forgerock/journey-client": "latest", "@forgerock/oidc-client": "latest", + "@forgerock/protect": "latest", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.19.0" diff --git a/javascript/reactjs-todo-journey/playwright.config.js b/javascript/reactjs-todo-journey/playwright.config.js index b6945e32..c055dd15 100644 --- a/javascript/reactjs-todo-journey/playwright.config.js +++ b/javascript/reactjs-todo-journey/playwright.config.js @@ -47,7 +47,6 @@ export default defineConfig({ VITE_SCOPE: 'profile openid email', VITE_WEB_OAUTH_CLIENT: 'WebOAuthClient', VITE_REALM_PATH: 'alpha', - VITE_CENTRALIZED_LOGIN: 'false', VITE_PINGONE_ENV_ID: '02fb4743-189a-4bc7-9d6c-a919edfe6447', }, ignoreHTTPSErrors: true, diff --git a/javascript/reactjs-todo-login-widget/.env.example b/javascript/reactjs-todo-login-widget/.env.example index 33137f40..8772de30 100644 --- a/javascript/reactjs-todo-login-widget/.env.example +++ b/javascript/reactjs-todo-login-widget/.env.example @@ -1,4 +1,3 @@ -# APP_URL= # not using this for preview-environment instead, we can use window.location.origin SERVER_URL= WEB_OAUTH_CLIENT= SCOPE='openid profile email' diff --git a/javascript/reactjs-todo-login-widget/README.md b/javascript/reactjs-todo-login-widget/README.md index 5d1da1f7..cf6efc9e 100644 --- a/javascript/reactjs-todo-login-widget/README.md +++ b/javascript/reactjs-todo-login-widget/README.md @@ -42,7 +42,6 @@ Change the name of `.env.example` to `.env` and replace the bracketed values (e. Example with annotations: ```text -APP_URL=https://localhost:8443 # in develop we do not use this variable for dynamic deployment reasons SERVER_URL=<<>> WEB_OAUTH_CLIENT=<<>> SCOPE='openid profile email' diff --git a/javascript/reactjs-todo-login-widget/client/README.md b/javascript/reactjs-todo-login-widget/client/README.md index 2efe18ee..d7d5b228 100644 --- a/javascript/reactjs-todo-login-widget/client/README.md +++ b/javascript/reactjs-todo-login-widget/client/README.md @@ -55,7 +55,6 @@ Example with annotations: ```text SERVER_URL=<<>> -APP_URL=https://react.example.com:8443 # in develop we do not use this variable for dynamic deployment reasons API_URL=https://api.example.com:9443 DEBUGGER_OFF=false JOURNEY_LOGIN=Login diff --git a/javascript/reactjs-todo-login-widget/webpack.config.js b/javascript/reactjs-todo-login-widget/webpack.config.js index 07c37246..6f00d45c 100644 --- a/javascript/reactjs-todo-login-widget/webpack.config.js +++ b/javascript/reactjs-todo-login-widget/webpack.config.js @@ -20,7 +20,6 @@ module.exports = () => { // Use process environment variables for prod, but fallback to local .env for dev const PORT = process.env.PORT || localEnv.PORT || '8443'; const SERVER_URL = process.env.SERVER_URL || localEnv.SERVER_URL; - const APP_URL = process.env.APP_URL || localEnv.APP_URL; const API_URL = process.env.API_URL || localEnv.API_URL; const DEBUGGER_OFF = process.env.DEBUGGER_OFF || localEnv.DEBUGGER_OFF; const DEVELOPMENT = process.env.DEVELOPMENT || localEnv.DEVELOPMENT; @@ -115,7 +114,6 @@ module.exports = () => { new webpack.DefinePlugin({ // Inject all the environment variable into the Webpack build 'process.env.SERVER_URL': JSON.stringify(SERVER_URL), - 'process.env.APP_URL': JSON.stringify(APP_URL), 'process.env.API_URL': JSON.stringify(API_URL), 'process.env.DEBUGGER_OFF': JSON.stringify(DEBUGGER_OFF), 'process.env.JOURNEY_LOGIN': JSON.stringify(JOURNEY_LOGIN), diff --git a/javascript/reactjs-todo-oidc/client/views/login.js b/javascript/reactjs-todo-oidc/client/views/login.js index 8e189cba..90a384fb 100755 --- a/javascript/reactjs-todo-oidc/client/views/login.js +++ b/javascript/reactjs-todo-oidc/client/views/login.js @@ -68,45 +68,41 @@ export default function Login() { useEffect(() => { async function handleCentralizedLogin() { - try { - if (codeParam && stateParam) { - /** - * When the user returns to this app after successfully logging in, - * the URL will include code and state query parameters that need to - * be passed in to complete the OAuth flow giving the user access - */ - setLoadingMessage('Success! Redirecting ...'); - await authorize(codeParam, stateParam); - } else if (errorParam) { - setLoadingMessage('Sign in failed. Please try again.'); - } else { - /** ***************************************************************** - * SDK INTEGRATION POINT - * Summary: Redirect the user to the authorization URL - * ------------------------------------------------------------------ - * Details: Use the OIDC client to get an authorization URL to redirect - * the user to sign in. - ***************************************************************** */ - if (DEBUGGER) debugger; - - setLoadingMessage('Redirecting ...'); + if (codeParam && stateParam) { + /** + * When the user returns to this app after successfully logging in, + * the URL will include code and state query parameters that need to + * be passed in to complete the OAuth flow giving the user access + */ + setLoadingMessage('Success! Redirecting ...'); + await authorize(codeParam, stateParam); + } else if (errorParam) { + setLoadingMessage('Sign in failed. Please try again.'); + } else { + /** ***************************************************************** + * SDK INTEGRATION POINT + * Summary: Redirect the user to the authorization URL + * ------------------------------------------------------------------ + * Details: Use the OIDC client to get an authorization URL to redirect + * the user to sign in. + ***************************************************************** */ + if (DEBUGGER) debugger; - const authorizeUrl = await oidcClient.authorize.url(); - if (typeof authorizeUrl !== 'string' && 'error' in authorizeUrl) { - setLoadingMessage('Sign in failed. Please try again.'); - console.error(`Error: centralized login; ${authorizeUrl.error}`); - return; - } + setLoadingMessage('Redirecting ...'); - window.location.assign(authorizeUrl); + const authorizeUrl = await oidcClient.authorize.url(); + if (typeof authorizeUrl !== 'string' && 'error' in authorizeUrl) { + setLoadingMessage('Sign in failed. Please try again.'); + console.error(`Error: centralized login; ${authorizeUrl.error}`); + return; } - } catch (error) { - console.error(`Error: centralized login; ${error}`); + + window.location.assign(authorizeUrl); } } handleCentralizedLogin(); - }, [authorize, codeParam, errorParam, methods, navigate, oidcClient, stateParam]); + }, [authorize, codeParam, errorParam, oidcClient, stateParam]); return (
diff --git a/javascript/reactjs-todo-oidc/e2e/oidc-login-pingone.spec.js b/javascript/reactjs-todo-oidc/e2e/oidc-login-pingone.spec.js index 5f1ced65..1b6775a3 100644 --- a/javascript/reactjs-todo-oidc/e2e/oidc-login-pingone.spec.js +++ b/javascript/reactjs-todo-oidc/e2e/oidc-login-pingone.spec.js @@ -53,7 +53,9 @@ test.describe('React - PingOne OIDC', () => { await page.getByRole('button', { name: 'Sign On' }).click(); await expect( - page.getByText(/Invalid username and\/or password|Validation Error/), + page.getByText( + /Invalid username and\/or password|Validation Error|identifier must be a uuid/, + ), ).toBeVisible(); }); diff --git a/javascript/reactjs-todo-oidc/package.json b/javascript/reactjs-todo-oidc/package.json index b69827c4..47ecfa64 100644 --- a/javascript/reactjs-todo-oidc/package.json +++ b/javascript/reactjs-todo-oidc/package.json @@ -39,7 +39,6 @@ }, "dependencies": { "@forgerock/oidc-client": "latest", - "@forgerock/protect": "latest", "cookie-parser": "^1.4.5", "cors": "^2.8.5", "dotenv": "^10.0.0",