Conversation
feat: add custom component pre-build registry generator feat: wire custom stage registry into mapStepToStage feat: wire custom callback registry into callback-mapper refactor: rename /user to /experimental/custom chore: add changeset docs: add Storybook story files to demo components refactor: move generateCustomRegistry.mjs to repo root for shared use feat: auto-filter callback props via acceptedProps registry feat: add $login-framework barrel for stable custom component imports docs: trim demo component comments to implementation-specific only docs: remove "stable" wording and reword experimental disclaimers
🦋 Changeset detectedLatest commit: d9aa93d The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
Pull request overview
This PR introduces an experimental custom-component extension surface for the login widget (with a generated registry and a curated $login-framework export module), and adds server-driven post-auth redirect handling in the login app with accompanying Playwright redirect tests.
Changes:
- Add a pre-build generator that scans
experimental/custom/{stages,callbacks}and produces a static custom component registry consumed by the widget runtime. - Add curated
$login-frameworkre-exports + Storybook demo templates for custom stage/callback overrides. - Implement server-side redirect resolution/validation flow in the login app and add Playwright E2E coverage for redirect behavior.
Reviewed changes
Copilot reviewed 39 out of 43 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/generateCustomRegistry.mjs | New generator script to build custom-registry.ts from custom Svelte components. |
| pnpm-lock.yaml | Updates Rollup wasm-node resolution version. |
| packages/login-widget/vitest.config.ts | Adds $login-framework alias + expands test inclusion globs. |
| packages/login-widget/vite.config.ts | Adds $login-framework alias for widget bundling. |
| packages/login-widget/vite.config.iife.ts | Adds $login-framework alias for IIFE build bundling. |
| packages/login-widget/package.json | Runs registry generator during widget prebuild. |
| package.json | Adds root generate:registry helper script. |
| experimental/tsconfig.json | New TS config for the experimental custom-component area and its aliases. |
| experimental/custom/stages/.gitkeep | Keeps the scanned stages directory tracked as scaffolding. |
| experimental/custom/README.md | Documentation for authoring custom stage/callback components. |
| experimental/custom/login-framework.ts | Curated public API surface re-exported as $login-framework. |
| experimental/custom/demo/stages/custom-login/custom-login.svelte | Demo custom stage override template (DefaultLogin). |
| experimental/custom/demo/stages/custom-login/custom-login.story.svelte | Storybook wrapper for the demo stage (store initialization + metadata). |
| experimental/custom/demo/stages/custom-login/custom-login.stories.js | Storybook stories for the demo stage template. |
| experimental/custom/demo/stages/custom-login/custom-login.mock.ts | Mock AM step payload for the demo stage. |
| experimental/custom/demo/README.md | How to use/copy demo templates into scanned directories. |
| experimental/custom/demo/callbacks/custom-name/custom-name.svelte | Demo custom callback override template (NameCallback). |
| experimental/custom/demo/callbacks/custom-name/custom-name.story.svelte | Storybook wrapper for the demo callback. |
| experimental/custom/demo/callbacks/custom-name/custom-name.stories.js | Storybook stories + interaction test for demo callback. |
| experimental/custom/demo/callbacks/custom-name/custom-name.mock.ts | Mock AM step payload for the demo callback. |
| experimental/custom/callbacks/.gitkeep | Keeps the scanned callbacks directory tracked as scaffolding. |
| e2e/tests/utilities/demo-user.js | Centralizes demo credentials for login-app redirect tests. |
| e2e/tests/login-app/redirect.test.ts | New Playwright tests covering success/invalid/empty redirect flows. |
| e2e/playwright.config.ts | Adds FR_OAUTH_SCOPE to avoid invalid_scope in AM flows. |
| core/style.store.ts | Adds StyleObject type alias for resolved widget style object. |
| core/journey/_utilities/map-stage.utilities.ts | Checks customStageRegistry before built-in stage mapping. |
| core/journey/_utilities/map-stage.utilities.test.ts | Mocks registry import to isolate mapping tests. |
| core/journey/_utilities/callback-mapper.svelte | Renders custom callback components first and forwards only accepted props. |
| apps/login-app/vite.config.ts | Adds $login-framework alias and allows serving ../../experimental. |
| apps/login-app/src/routes/(app)/success-redirect/+page.svelte | New fallback page shown when success redirect can’t be performed. |
| apps/login-app/src/routes/(app)/failure-redirect/+page.svelte | New fallback page shown when failure redirect can’t be performed. |
| apps/login-app/src/routes/(app)/+page.svelte | Submits hidden form on journey completion to trigger server-side redirect. |
| apps/login-app/src/routes/(app)/+page.server.ts | Stores redirect params and adds a default server action to perform redirects. |
| apps/login-app/src/lib/server/sessions.ts | Adds cookie helpers + AM fetch utilities + role lookup via session token. |
| apps/login-app/src/lib/server/redirect/redirect.ts | Implements server-side redirect selection/validation logic. |
| apps/login-app/src/lib/server/redirect/README.md | Documents redirect precedence and supported flows. |
| apps/login-app/src/lib/server/_utilities.ts | Adds redirect utilities (form parsing, URL checks, role-based destination, etc.). |
| apps/login-app/src/lib/server/_utilities.test.ts | Adds unit tests for redirect utility helpers. |
| .storybook/main.js | Includes experimental custom stories + $login-framework alias. |
| .gitignore | Ignores generated registry and user-authored custom components under scanned dirs. |
| .github/workflows/docker-branch.yml | Adds a branch-specific Docker build/push workflow. |
| .github/workflows/ci.yml | Runs registry generator before checks/tests/Chromatic in CI. |
| .changeset/add-custom-components.md | Announces minor release change for custom component support. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (3)
apps/login-app/src/lib/server/_utilities.ts:43
extractDomainFromUrlnow accepts an arbitraryurlparameter, but the thrown error messages still referenceAM_DOMAIN_PATH. This makes failures misleading when callers pass other URLs. Update the error text to refer to the actual input (e.g., "url is not a string" / "url is not a valid URL").
export function extractDomainFromUrl(url: unknown) {
if (typeof url !== 'string') {
throw new Error('AM_DOMAIN_PATH is not a string');
}
/**
* Good old Stack Overflow answer: https://stackoverflow.com/a/25703406
*
* Demo: https://regex101.com/r/wN6cZ7/365
*/
const arr = url.match(/^(?:https?:\/\/)?(?:[^@/\n]+@)?(?:www\.)?([^:/?\n]+)/);
if ((!Array.isArray(arr) && !arr) || !arr[1]) {
throw new Error('AM_DOMAIN_PATH is not a valid URL');
}
packages/login-widget/package.json:34
- The custom registry file (
core/journey/_utilities/custom-registry.ts) is now a required import for the widget, butpnpm --filter @forgerock/login-widget run testwon't runprebuild. This means a clean checkout running tests will fail unless the generator is run manually. Consider adding apretesthook (or updating the roottestscript) to runnode ../../scripts/generateCustomRegistry.mjs/pnpm generate:registrybefore Vitest.
package.json:17 - A new
generate:registryscript was added, but it isn't wired into common workflows that need the generatedcore/journey/_utilities/custom-registry.ts(e.g.test,storybook,check:lintif TS typecheck runs). Sincecustom-registry.tsis gitignored and required at build time, consider invoking this script automatically in the relevant root scripts (or as apostinstall) to avoid "module not found" errors on fresh checkouts.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /** | ||
| * | ||
| * Copyright © 2026 Ping Identity Corporation. All right reserved. | ||
| * | ||
| * This software may be modified and distributed under the terms | ||
| * of the MIT license. See the LICENSE file for details. | ||
| * | ||
| **/ | ||
|
|
There was a problem hiding this comment.
The file starts with a JS block comment (/** ... */) at the top level of the Svelte template. Outside of <script>/<style>, this will be treated as markup text and can render on the page (or fail linting/formatting), unlike the HTML comment headers used elsewhere. Convert it to an HTML comment (<!-- ... -->) or move it inside the <script> block.
| /** | |
| * | |
| * Copyright © 2026 Ping Identity Corporation. All right reserved. | |
| * | |
| * This software may be modified and distributed under the terms | |
| * of the MIT license. See the LICENSE file for details. | |
| * | |
| **/ | |
| <!-- | |
| Copyright © 2026 Ping Identity Corporation. All right reserved. | |
| This software may be modified and distributed under the terms | |
| of the MIT license. See the LICENSE file for details. | |
| --> |
| /** | ||
| * | ||
| * Copyright © 2026 Ping Identity Corporation. All right reserved. | ||
| * | ||
| * This software may be modified and distributed under the terms | ||
| * of the MIT license. See the LICENSE file for details. | ||
| * | ||
| **/ | ||
|
|
There was a problem hiding this comment.
The file starts with a JS block comment (/** ... */) at the top level of the Svelte template. Outside of <script>/<style>, this will be treated as markup text and can render on the page (or fail linting/formatting). Use an HTML comment header (<!-- ... -->) like other Svelte files, or move the comment inside <script>.
| /** | |
| * | |
| * Copyright © 2026 Ping Identity Corporation. All right reserved. | |
| * | |
| * This software may be modified and distributed under the terms | |
| * of the MIT license. See the LICENSE file for details. | |
| * | |
| **/ | |
| <!-- | |
| Copyright © 2026 Ping Identity Corporation. All right reserved. | |
| This software may be modified and distributed under the terms | |
| of the MIT license. See the LICENSE file for details. | |
| --> |
| if (successUrl && isDefaultPath(successUrl)) { | ||
| if (isGotoOnFail) { | ||
| successUrl = null; // treat as unusable | ||
| } else if (journeyStepUrl && !isDefaultPath(journeyStepUrl)) { | ||
| throw redirect(303, resolveAgainstOrigin(journeyStepUrl, amOrigin)); | ||
| } else if (isSamlURL(gotoUrl)) { | ||
| throw redirect(303, resolveAgainstOrigin(gotoUrl, amOrigin)); | ||
| } |
There was a problem hiding this comment.
journeyStepUrl is client-provided form data and is redirected to without any server-side validation (only resolved against amOrigin). Because resolveAgainstOrigin preserves absolute URLs, an attacker can POST a crafted journeyStepUrl to trigger an open redirect. Consider ignoring journeyStepUrl from the client entirely, or validating it with validateGoto, or strictly allowing only same-origin relative paths before redirecting.
| // Only use journeyStepUrl as a destination for SAML flows where AM itself | ||
| // sets a meaningful SP-initiated return URL. For standard logins without an | ||
| // explicit ?goto, journeyStepUrl is AM's realm default (the login app URL) | ||
| // and must not be used — fall through to role-based redirect instead. | ||
| if (journeyStepUrl && isSamlURL(journeyStepUrl)) { | ||
| throw redirect(303, resolveAgainstOrigin(journeyStepUrl, amOrigin)); | ||
| } |
There was a problem hiding this comment.
The SAML fallback redirects to gotoUrl / journeyStepUrl based on substring checks (isSamlURL) even when validateGoto produced a default/console URL. Since goto comes from user-controlled query params and resolveAgainstOrigin keeps absolute URLs, this can bypass validation and become an open redirect (e.g., https://attacker.tld/saml2). Tighten this by ensuring the destination is same-origin/relative, or by validating the original URL with AM before using it.
| test: { | ||
| include: ['**/*.test.ts'], | ||
| include: [ | ||
| resolve('../../core/**/*.test.ts'), | ||
| resolve('../../experimental/custom/**/*.test.ts'), | ||
| ], | ||
| exclude: ['node_modules', 'dist'], |
There was a problem hiding this comment.
test.include is being built with path.resolve('../../core/**/*.test.ts'). On Windows this produces backslashes in the glob (e.g. C:\...\core\**\*.test.ts), which can break glob matching depending on the globber. Prefer forward-slash globs (e.g. plain '../../core/**/*.test.ts') or normalize the resolved path to POSIX separators before passing to Vitest.
| Every `.svelte` file must begin with an HTML comment block that declares its type and name: | ||
|
|
||
| ```svelte | ||
| <!-- | ||
| @component | ||
| Type: stage | ||
| Name: DefaultLogin | ||
| --> | ||
|
|
||
| <script> | ||
| let { componentStyle, form, formEl, journey, metadata, step } = $props(); | ||
| </script> | ||
|
|
There was a problem hiding this comment.
The docs' stage example uses the Svelte 5 $props() pattern, but the build-time registry generation and custom callback forwarding logic rely on detecting export let props (to populate acceptedProps). If callback components use $props() instead, acceptedProps will be empty and the framework will forward no props to the custom component. Either document that custom callback components must use export let for all props, or update the generator/mapper to support $props()-based prop discovery.
| name: Docker Branch Build | ||
|
|
||
| on: | ||
| push: | ||
| branches: | ||
| - test-login-redirect | ||
| workflow_dispatch: | ||
|
|
There was a problem hiding this comment.
This workflow is hard-wired to run only on pushes to a specific branch name (test-login-redirect) and pushes an image tagged :test-login-redirect. If this is meant to be temporary for a feature branch, it will be dead code after merge and adds long-term CI maintenance overhead. Consider removing it from the mainline PR, or generalizing it to run on PRs / a tag input (workflow_dispatch) with a parameterized tag.
3fdcfd7 to
713bf4e
Compare
fecab8c to
d9aa93d
Compare
No description provided.