Skip to content

Test login redirect#460

Draft
livstowe wants to merge 5 commits intomainfrom
test-login-redirect
Draft

Test login redirect#460
livstowe wants to merge 5 commits intomainfrom
test-login-redirect

Conversation

@livstowe
Copy link
Copy Markdown
Collaborator

@livstowe livstowe commented Apr 8, 2026

No description provided.

vatsalparikh and others added 4 commits March 30, 2026 16:45
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
Copilot AI review requested due to automatic review settings April 8, 2026 19:31
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 8, 2026

🦋 Changeset detected

Latest commit: d9aa93d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@forgerock/login-widget Minor

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

@livstowe livstowe marked this pull request as draft April 8, 2026 19:32
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-framework re-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

  • extractDomainFromUrl now accepts an arbitrary url parameter, but the thrown error messages still reference AM_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, but pnpm --filter @forgerock/login-widget run test won't run prebuild. This means a clean checkout running tests will fail unless the generator is run manually. Consider adding a pretest hook (or updating the root test script) to run node ../../scripts/generateCustomRegistry.mjs/pnpm generate:registry before Vitest.
    package.json:17
  • A new generate:registry script was added, but it isn't wired into common workflows that need the generated core/journey/_utilities/custom-registry.ts (e.g. test, storybook, check:lint if TS typecheck runs). Since custom-registry.ts is gitignored and required at build time, consider invoking this script automatically in the relevant root scripts (or as a postinstall) to avoid "module not found" errors on fresh checkouts.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1 to +9
/**
*
* 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.
*
**/

Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
/**
*
* 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.
-->

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +9
/**
*
* 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.
*
**/

Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>.

Suggested change
/**
*
* 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.
-->

Copilot uses AI. Check for mistakes.
Comment on lines +90 to +97
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));
}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +106
// 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));
}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 16 to 21
test: {
include: ['**/*.test.ts'],
include: [
resolve('../../core/**/*.test.ts'),
resolve('../../experimental/custom/**/*.test.ts'),
],
exclude: ['node_modules', 'dist'],
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +37
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>

Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +8
name: Docker Branch Build

on:
push:
branches:
- test-login-redirect
workflow_dispatch:

Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@livstowe livstowe force-pushed the test-login-redirect branch from 3fdcfd7 to 713bf4e Compare April 8, 2026 21:27
@livstowe livstowe force-pushed the test-login-redirect branch from fecab8c to d9aa93d Compare April 9, 2026 16:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants