Skip to content

feat(journey-client): add hasPasskeyAutocompleteValues to WebAuthn#720

Open
vatsalparikh wants to merge 1 commit into
mainfrom
SDKS-5020
Open

feat(journey-client): add hasPasskeyAutocompleteValues to WebAuthn#720
vatsalparikh wants to merge 1 commit into
mainfrom
SDKS-5020

Conversation

@vatsalparikh

@vatsalparikh vatsalparikh commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds passkey-autofill support to the journey-app WebAuthn flow, backed by two new public helpers on the WebAuthn class and an optional mediationOverride parameter on authenticate().

The journey-app now fires WebAuthn once, non-blocking, on every authentication step. The SDK decides silent (conditional mediation) vs. modal prompt internally from the step metadata, so the app never has to branch on mediation. A manual "Sign in with a passkey" button is rendered only for passkey-autofill steps, and clicking it forces a modal prompt via mediationOverride.

SDK changes (@forgerock/journey-client)

  • WebAuthn.hasPasskeyAutocompleteValues(step) — returns true when a NameCallback carries both username and webauthn autocomplete values (AM's signal that the step supports passkey autofill).
  • WebAuthn.hasAuthenticationButton(step) — returns true when the metadata sets manualButtonEnabled.
  • WebAuthn.authenticate(step, signal?, mediationOverride?) — new optional mediationOverride. When provided it takes precedence over the metadata mediation (mediationOverride ?? meta.mediation), letting the manual button force a modal (non-conditional) prompt even when the server requested conditional mediation. Server behavior is unchanged when the argument is omitted.

The three AM config flags

Passkey-autofill behavior is driven by three server-controlled signals in the authentication step:

Flag Source Meaning
autocomplete NameCallback autocompleteValues contains username + webauthn The step supports passkey autofill; the username input should be decorated with autocomplete="username webauthn". Detected via hasPasskeyAutocompleteValues.
conditional metadata mediation: 'conditional' The SDK runs navigator.credentials.get() with conditional mediation → silent autofill (no modal), surfaced inline in the username field.
button metadata manualButtonEnabled: true Render a "Sign in with a passkey" button as an explicit fallback. Detected via hasAuthenticationButton.

autocomplete gates the whole passkey path: without it, the input is not decorated and the button is never rendered, regardless of the other two flags. Note also that AM only sends manualButtonEnabled: true together with conditional mediation — the two are coupled server-side.

Behavior across all 8 combinations

authenticate() is fired on render for every authentication step, non-blocking. The SDK then decides silent vs. modal from the metadata mediation: mediation === 'conditional' runs a silent conditional request; anything else runs a modal navigator.credentials.get(). There is no submit-triggered prompt — the WebAuthn request always starts on render.

isConditionalSupported is a separate browser capability check, orthogonal to these three AM flags; the table below assumes a browser that supports conditional mediation.

The autofill input is decorated exactly when autocomplete is present, so it is not shown as a separate column.

Case autocomplete conditional button WebAuthn request on render Passkey button rendered?
000 modal prompt (non-conditional)
100 modal prompt (non-conditional)
010 silent conditional request (SDK uses conditional mediation; no decorated input to attach a dropdown to)
001 modal prompt (non-conditional) — button never rendered without autocomplete
110 silent conditional autofill
101 modal prompt (non-conditional); AM sends manualButtonEnabled: false without conditional mediation
011 silent conditional request — button never rendered without autocomplete
111 silent conditional autofill, manual button available as fallback

Key invariants:

  • The Submit button always renders (traditional login stays available in every case), because authenticate() is fired without await and the handler returns immediately.
  • The passkey button appears only when autocomplete is present — manualButtonEnabled alone is insufficient (see 001 / 011).
  • Clicking the passkey button (cases where it renders) aborts the background conditional request and forces a modal prompt (mediationOverride: 'optional'), so the user can explicitly pick a passkey.

Tests

e2e/journey-suites/src/webauthn-device.test.ts covers all 8 cases plus two variants of the full-feature (111) case:

  • 111a — silent passkey auth via the decorated username field (the autofill path).
  • 111b — explicit auth by clicking "Sign in with a passkey", exercising the forced-modal mediationOverride path end to end.

The autofill cases fire WebAuthn on render, which the auto-resolving virtual authenticator would otherwise complete before the transient UI can be asserted. A setAutomaticPresenceSimulation(false) helper holds the request open so the rendered input/button can be checked deterministically. Silent conditional completion depends on real autofill-dropdown interaction that the virtual authenticator cannot drive, so the autofill cases assert the rendered UI rather than final login.

Unit tests in packages/journey-client/src/lib/webauthn/webauthn.test.ts cover hasPasskeyAutocompleteValues and hasAuthenticationButton.

🤖 Generated with Claude Code

@changeset-bot

changeset-bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5472d71

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

This PR includes changesets to release 12 packages
Name Type
@forgerock/journey-client Minor
@forgerock/davinci-client Minor
@forgerock/device-client Minor
@forgerock/oidc-client Minor
@forgerock/protect Minor
@forgerock/sdk-types Minor
@forgerock/sdk-utilities Minor
@forgerock/iframe-manager Minor
@forgerock/sdk-logger Minor
@forgerock/sdk-oidc Minor
@forgerock/sdk-request-middleware Minor
@forgerock/storage 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

@nx-cloud

nx-cloud Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 5472d71

Command Status Duration Result
nx run-many -t build --no-agents ✅ Succeeded <1s View ↗
nx affected -t build lint test typecheck e2e-ci ✅ Succeeded 1m 31s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-07-06 23:38:02 UTC

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an optional mediationOverride parameter to WebAuthn.authenticate() allowing callers to force mediation behavior, plus new hasAuthenticationButton and hasPasskeyAutocompleteValues helpers and a manualButtonEnabled metadata field. Updates e2e components/tests to support a manual "Sign in with a passkey" button alongside background conditional mediation.

Changes

WebAuthn Mediation Override and Manual Passkey Button

Layer / File(s) Summary
Core API: mediation override and helper methods
packages/journey-client/src/lib/webauthn/webauthn.ts, packages/journey-client/src/lib/webauthn/interfaces.ts
authenticate() gains an optional mediationOverride parameter that takes precedence over step metadata mediation; adds hasAuthenticationButton and hasPasskeyAutocompleteValues static helpers and a manualButtonEnabled interface field.
API report, mocks, and unit tests
packages/journey-client/api-report/journey-client.webauthn.api.md, packages/journey-client/src/lib/webauthn/webauthn.mock.data.ts, packages/journey-client/src/lib/webauthn/webauthn.test.ts, .changeset/webauthn-mediation-override.md
API report reflects new signature/helpers/field; adds mock metadata callbacks with/without passkey autofill; adds tests for new helpers using typed Step casts; documents change in a changeset.
e2e webauthn-step conditional/manual button flow
e2e/journey-app/components/webauthn-step.ts, e2e/journey-app/components/text-input.ts
Refactors Authentication flow to render callbacks upfront, run conditional mediation in the background via AbortController, and conditionally show a manual "Sign in with a passkey" button that aborts background auth and re-authenticates with 'optional' mediation; removes the fixed webauthn autocomplete assignment from the text input component.
e2e conditional autofill test matrix
e2e/journey-suites/src/webauthn-device.test.ts
Replaces a skipped test with a deterministic matrix of tests using new setPresenceSimulation and registerPasskey helpers, asserting autofill input/button visibility across scenarios.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant WebAuthnStep
  participant WebAuthn
  participant AbortController

  WebAuthnStep->>WebAuthn: isConditionalMediationSupported()
  WebAuthnStep->>AbortController: create controller
  WebAuthnStep->>WebAuthn: authenticate(step, signal) [background]
  alt background succeeds
    WebAuthn-->>WebAuthnStep: success
    WebAuthnStep->>WebAuthnStep: submitForm()
  else user clicks manual button
    User->>WebAuthnStep: click "Sign in with a passkey"
    WebAuthnStep->>AbortController: abort()
    WebAuthnStep->>WebAuthn: authenticate(step, undefined, 'optional')
    WebAuthn-->>WebAuthnStep: result
    WebAuthnStep->>WebAuthnStep: submitForm() or setError()
  end
Loading

Possibly related PRs

  • ForgeRock/ping-javascript-sdk#581: Modifies the same WebAuthn conditional-mediation authentication flow, including WebAuthn.authenticate mediation/AbortSignal handling and e2e webauthn-step conditional autofill logic that this PR builds on.

Suggested reviewers: ancheetah, SteinGabriel, ryanbas21, cerebrl

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is specific and accurately reflects one real part of the change, though it omits the broader passkey-authentication updates.
Description check ✅ Passed The description is thorough and covers the changes, behavior, and tests, but it uses 'Summary' instead of the template's 'Description' section.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch SDKS-5020

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/journey-client/src/lib/webauthn/webauthn.ts (1)

190-233: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Abort the prior conditional request before the modal fallback.
mediationOverride can send a second authenticate() call down the non-conditional path while this.conditionalAbortController is still pending, so the original conditional navigator.credentials.get() keeps running alongside the new modal request. Abort the stored controller here too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/journey-client/src/lib/webauthn/webauthn.ts` around lines 190 - 233,
The authentication flow in `getAuthenticationOutcome` should also cancel any
in-flight conditional request when `mediationOverride` routes a retry into the
non-conditional/modal path. Update the logic around
`this.conditionalAbortController`, `mediationOverride`, and
`getAuthenticationCredential` so the stored controller is aborted before
starting the fallback request, not only in the `mediation === 'conditional'`
branch.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@e2e/journey-app/components/webauthn-step.ts`:
- Around line 68-72: The WebAuthn error handler in WebAuthn.authenticate should
not surface an error for the intentional conditional-request abort. Update the
catch branch in webauthn-step’s submit flow to inspect the thrown error and skip
setError when it is an AbortError, while still showing the existing failure
message for real authentication problems so the manual fallback can continue
cleanly.

---

Outside diff comments:
In `@packages/journey-client/src/lib/webauthn/webauthn.ts`:
- Around line 190-233: The authentication flow in `getAuthenticationOutcome`
should also cancel any in-flight conditional request when `mediationOverride`
routes a retry into the non-conditional/modal path. Update the logic around
`this.conditionalAbortController`, `mediationOverride`, and
`getAuthenticationCredential` so the stored controller is aborted before
starting the fallback request, not only in the `mediation === 'conditional'`
branch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 60f2d951-4056-456f-ace4-cd7b14d14fb0

📥 Commits

Reviewing files that changed from the base of the PR and between 2f91101 and 7c19dea.

📒 Files selected for processing (9)
  • .changeset/webauthn-mediation-override.md
  • e2e/journey-app/components/text-input.ts
  • e2e/journey-app/components/webauthn-step.ts
  • e2e/journey-suites/src/webauthn-device.test.ts
  • packages/journey-client/api-report/journey-client.webauthn.api.md
  • packages/journey-client/src/lib/webauthn/interfaces.ts
  • packages/journey-client/src/lib/webauthn/webauthn.mock.data.ts
  • packages/journey-client/src/lib/webauthn/webauthn.test.ts
  • packages/journey-client/src/lib/webauthn/webauthn.ts
💤 Files with no reviewable changes (1)
  • e2e/journey-app/components/text-input.ts

Comment on lines +68 to +72
void WebAuthn.authenticate(step, controller.signal)
.then(() => submitForm())
.catch(() => {
setError('WebAuthn failed or was cancelled. Please try again or use a different method.');
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant files and show compact structure first.
git ls-files | rg '(^|/)e2e/journey-app/components/webauthn-step\.ts$|(^|/)webauthn|(^|/)passkey|(^|/)auth' || true

echo '--- outline: e2e/journey-app/components/webauthn-step.ts ---'
ast-grep outline e2e/journey-app/components/webauthn-step.ts --view expanded || true

# Read the relevant slice with line numbers.
echo '--- webauthn-step.ts (selected lines) ---'
sed -n '1,180p' e2e/journey-app/components/webauthn-step.ts | cat -n

# Search for the controller abort and authenticate implementation/usages.
echo '--- search: controller.abort / AbortError / WebAuthn.authenticate ---'
rg -n "controller\.abort|AbortError|WebAuthn\.authenticate|mediation === 'conditional'|conditional" e2e/journey-app . --glob '!**/node_modules/**' || true

# If WebAuthn.authenticate is in a nearby file, inspect it.
AUTH_FILE=$(rg -l "class WebAuthn|function authenticate|const WebAuthn|authenticate\\(" e2e/journey-app . --glob '!**/node_modules/**' | head -n 20 | tr '\n' ' ')
echo "--- candidate auth files: ${AUTH_FILE} ---"
for f in $AUTH_FILE; do
  echo "--- $f ---"
  sed -n '1,260p' "$f" | cat -n | sed -n '1,260p'
done

Repository: ForgeRock/ping-javascript-sdk

Length of output: 50386


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- packages/journey-client/src/lib/webauthn/webauthn.ts (relevant slices) ---'
sed -n '160,270p' packages/journey-client/src/lib/webauthn/webauthn.ts | cat -n
echo
sed -n '610,670p' packages/journey-client/src/lib/webauthn/webauthn.ts | cat -n

echo '--- e2e/journey-suites/src/webauthn-device.test.ts (conditional/passkey cases) ---'
sed -n '300,380p' e2e/journey-suites/src/webauthn-device.test.ts | cat -n

Repository: ForgeRock/ping-javascript-sdk

Length of output: 11725


Ignore AbortError from the background conditional request
e2e/journey-app/components/webauthn-step.ts:68-72 will show “WebAuthn failed or was cancelled…” when the passkey button intentionally aborts the in-flight conditional request. Skip setError(...) for AbortError here so the manual flow can continue cleanly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/journey-app/components/webauthn-step.ts` around lines 68 - 72, The
WebAuthn error handler in WebAuthn.authenticate should not surface an error for
the intentional conditional-request abort. Update the catch branch in
webauthn-step’s submit flow to inspect the thrown error and skip setError when
it is an AbortError, while still showing the existing failure message for real
authentication problems so the manual fallback can continue cleanly.

@pkg-pr-new

pkg-pr-new Bot commented Jul 6, 2026

Copy link
Copy Markdown

Open in StackBlitz

@forgerock/davinci-client

pnpm add https://pkg.pr.new/@forgerock/davinci-client@720

@forgerock/device-client

pnpm add https://pkg.pr.new/@forgerock/device-client@720

@forgerock/journey-client

pnpm add https://pkg.pr.new/@forgerock/journey-client@720

@forgerock/oidc-client

pnpm add https://pkg.pr.new/@forgerock/oidc-client@720

@forgerock/protect

pnpm add https://pkg.pr.new/@forgerock/protect@720

@forgerock/sdk-types

pnpm add https://pkg.pr.new/@forgerock/sdk-types@720

@forgerock/sdk-utilities

pnpm add https://pkg.pr.new/@forgerock/sdk-utilities@720

@forgerock/iframe-manager

pnpm add https://pkg.pr.new/@forgerock/iframe-manager@720

@forgerock/sdk-logger

pnpm add https://pkg.pr.new/@forgerock/sdk-logger@720

@forgerock/sdk-oidc

pnpm add https://pkg.pr.new/@forgerock/sdk-oidc@720

@forgerock/sdk-request-middleware

pnpm add https://pkg.pr.new/@forgerock/sdk-request-middleware@720

@forgerock/storage

pnpm add https://pkg.pr.new/@forgerock/storage@720

commit: 5472d71

@codecov-commenter

codecov-commenter commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 23.16%. Comparing base (eafe277) to head (5472d71).
⚠️ Report is 42 commits behind head on main.

❌ Your project status has failed because the head coverage (23.16%) is below the target coverage (40.00%). You can increase the head coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #720      +/-   ##
==========================================
+ Coverage   18.07%   23.16%   +5.09%     
==========================================
  Files         155      161       +6     
  Lines       24398    25612    +1214     
  Branches     1203     1617     +414     
==========================================
+ Hits         4410     5934    +1524     
+ Misses      19988    19678     -310     
Files with missing lines Coverage Δ
...ckages/journey-client/src/lib/webauthn/webauthn.ts 18.07% <100.00%> (+2.38%) ⬆️

... and 16 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Deployed b308f69 to https://ForgeRock.github.io/ping-javascript-sdk/pr-720/b308f69db4859ff302f25d7d63f29af7e4702677 branch gh-pages in ForgeRock/ping-javascript-sdk

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle Size Analysis

📦 Bundle Size Analysis

🚨 Significant Changes

🔻 @forgerock/device-client - 0.0 KB (-10.0 KB, -100.0%)
🔻 @forgerock/journey-client - 0.0 KB (-92.6 KB, -100.0%)

📊 Minor Changes

📉 @forgerock/device-client - 10.0 KB (-0.0 KB)
📈 @forgerock/journey-client - 93.1 KB (+0.5 KB)

➖ No Changes

@forgerock/oidc-client - 35.3 KB
@forgerock/sdk-types - 9.1 KB
@forgerock/davinci-client - 54.4 KB
@forgerock/sdk-utilities - 18.6 KB
@forgerock/protect - 144.6 KB
@forgerock/iframe-manager - 3.2 KB
@forgerock/sdk-logger - 1.6 KB
@forgerock/storage - 1.5 KB
@forgerock/sdk-request-middleware - 4.6 KB
@forgerock/sdk-oidc - 5.7 KB


14 packages analyzed • Baseline from latest main build

Legend

🆕 New package
🔺 Size increased
🔻 Size decreased
➖ No change

ℹ️ How bundle sizes are calculated
  • Current Size: Total gzipped size of all files in the package's dist directory
  • Baseline: Comparison against the latest build from the main branch
  • Files included: All build outputs except source maps and TypeScript build cache
  • Exclusions: .map, .tsbuildinfo, and .d.ts.map files

🔄 Updated automatically on each push to this PR

@nx-cloud nx-cloud Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Important

At least one additional CI pipeline execution has run since the conclusion below was written and it may no longer be applicable.

Nx Cloud is proposing a fix for your failed CI:

We updated the "Authenticate with the registered WebAuthn device" test step to align with the PR's new non-blocking WebAuthn behaviour, where authenticate() fires on render and the virtual authenticator auto-resolves it, calling submitForm() in the background. The old test was manually filling the username and clicking Submit — a pattern from the previous sequential flow — which raced with the background auto-resolve, causing the form to be submitted without the WebAuthn outcome set. Removing the manual interactions and simply waiting for the Logout button matches what all four new conditional-autofill tests (000–110) already do.

Warning

We could not verify this fix.

diff --git a/e2e/journey-suites/src/webauthn-device.test.ts b/e2e/journey-suites/src/webauthn-device.test.ts
index 52538a0..9941523 100644
--- a/e2e/journey-suites/src/webauthn-device.test.ts
+++ b/e2e/journey-suites/src/webauthn-device.test.ts
@@ -92,9 +92,8 @@ test.describe('WebAuthn register, authenticate, and delete device', () => {
       await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible();
 
       await navigate(authenticationUrl);
-      await expect(page.getByLabel('User Name')).toBeVisible();
-      await page.getByLabel('User Name').fill(username);
-      await clickButton('Submit', '/authenticate');
+      // WebAuthn fires automatically on render. The virtual authenticator auto-resolves it,
+      // calling submitForm() in the background — no manual form interaction is needed.
       await expect(page.getByRole('button', { name: 'Logout' })).toBeVisible();
     });
 

Apply fix via Nx Cloud  Reject fix via Nx Cloud


Or Apply changes locally with:

npx nx-cloud apply-locally vWFv-39tY

Apply fix locally with your editor ↗   View interactive diff ↗



🎓 Learn more about Self-Healing CI on nx.dev

@vatsalparikh vatsalparikh changed the title feat(journey-client): add hasAuthenticationButton and hasPasskeyAutocompleteValues to WebAuthn feat(journey-client): add hasPasskeyAutocompleteValues to WebAuthn Jul 6, 2026

@SteinGabriel SteinGabriel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes look good. There's a stale copyright date in webauthn.mock.data.ts, but other than that it looks good to merge.

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.

3 participants