Skip to content

IS-11252 WebAuthn client operations in the LWA#154

Merged
aleixsuau merged 12 commits into
integration/IS-5161/login-web-appfrom
feature/IS-11252/webauthn-client-operations
May 22, 2026
Merged

IS-11252 WebAuthn client operations in the LWA#154
aleixsuau merged 12 commits into
integration/IS-5161/login-web-appfrom
feature/IS-11252/webauthn-client-operations

Conversation

@aleixsuau
Copy link
Copy Markdown
Contributor

@aleixsuau aleixsuau commented May 5, 2026

Jira: https://curity.atlassian.net/browse/IS-11252

Adds full support for webauthn-registration and webauthn-authentication HAAPI client operations, including the any-device-mode where the user picks between platform and cross-platform credentials.

Part 1 — WebAuthn pipeline

1. Format — split the action. formatNextStepData runs each webauthn-registration action through splitWebAuthnRegistrationAction. Any-device-mode with both options becomes two sibling actions, each titled "This device" / "Another device". Single-option and passkeys-mode pass through.

2. Render — one button per option, capability-gated. The default rendering pipeline produces one button per emitted action. useIsClientOperationAvailable disables it when the WebAuthn API is missing, or — for platform-only any-device registration — when no user-verifying platform authenticator is available.

3. Click — run the ceremony. performClientOperation dispatches to runWebAuthnRegistration (calls navigator.credentials.create(), serialises under the matching credential / platformCredential / crossPlatformCredential payload key) or runWebAuthnAuthentication (calls navigator.credentials.get(), serialises under credential). Both honour the AbortSignal and resume the flow via continueActions[0].

Part 2 — Folder refactor (no behaviour change)

feature/actions/client-operation/ reorganized so each operation owns its own folder/file as webauthn.

Test plan

  • passkeys-mode registration — single button, ceremony completes
  • any-device with both options — two buttons; each runs its matching ceremony
  • platform-only on a device without platform authenticator — button disabled
  • cross-platform-only — button always enabled
  • webauthn-authentication — ceremony completes
  • WebAuthn-unsupported browser — WebAuthn buttons disabled; BankID / external-browser-flow unaffected

Follow-ups (separate PRs)

  • Error handling — runners currently throw on user-cancel, hardware error, or server rejection; no mapping to errorActions or in-flow user feedback yet.
  • Auto-triggering — analogous to bankIdAutostart: a config flag to start the ceremony immediately on step entry.

Implements the webauthn-registration and webauthn-authentication HAAPI
client operations, including any-device-mode that may offer the user a
choice between platform and cross-platform credentials.

The feature comes with a folder restructure: per-operation modules live
under a new operations/ subfolder, with the previous client-operations.ts
god-file slimmed to a thin dispatcher.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Any-device-mode `webauthn-registration` actions split into two siblings
now read e.g. "Register new device (This device)" /
"Register new device (Another device)" instead of just the device label,
preserving the server-supplied original title.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

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

Adds full HAAPI WebAuthn client-operation support (registration + authentication), including any-device mode that renders separate “platform” vs “cross-platform” choices, and refactors the client-operation implementation into per-operation modules.

Changes:

  • Add WebAuthn registration/authentication runners, plus action-splitting for any-device registration so the UI can render one button per option.
  • Introduce runtime capability gating in the default client-operation UI (disable WebAuthn actions when unsupported / when platform authenticator is unavailable for platform-only any-device registration).
  • Refactor client-operation code into feature/actions/client-operation/operations/* and update imports/exports/tests accordingly.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/login-web-app/src/haapi-stepper/feature/stepper/HaapiStepper.tsx Updates client-operation import to the refactored operations entrypoint.
src/login-web-app/src/haapi-stepper/feature/stepper/HaapiStepper.spec.tsx Updates BankID app opener mocking path after refactor.
src/login-web-app/src/haapi-stepper/feature/stepper/data-formatters/polling-step.ts Uses new BankID operation exports for polling autostart behavior.
src/login-web-app/src/haapi-stepper/feature/stepper/data-formatters/format-next-step-data.ts Adds WebAuthn registration action splitting to emit one action per credential option.
src/login-web-app/src/haapi-stepper/feature/index.ts Re-exports refactored client-operations entrypoint.
src/login-web-app/src/haapi-stepper/feature/actions/client-operation/HaapiStepperClientOperationUI.tsx Disables client-operation button based on runtime availability; removes render prop.
src/login-web-app/src/haapi-stepper/feature/actions/client-operation/HaapiStepperClientOperationUI.spec.tsx Adds tests for default rendering and WebAuthn any-device split integration.
src/login-web-app/src/haapi-stepper/feature/actions/client-operation/useIsClientOperationAvailable.ts New availability hook for capability-gating client-operation actions.
src/login-web-app/src/haapi-stepper/feature/actions/client-operation/operations/client-operations.ts New dispatcher for client operations (BankID, external browser flow, WebAuthn).
src/login-web-app/src/haapi-stepper/feature/actions/client-operation/operations/external-browser-flow.ts Extracted external-browser-flow runner and type guard.
src/login-web-app/src/haapi-stepper/feature/actions/client-operation/operations/bankid/* Extracted BankID operation (runner, opener, type guard).
src/login-web-app/src/haapi-stepper/feature/actions/client-operation/operations/webauthn/* Implements WebAuthn runners, action splitting helpers, and platform authenticator availability hook.
src/login-web-app/src/haapi-stepper/feature/actions/client-operation/openBankIdApp.ts Removed old BankID opener (moved into operations/bankid).
src/login-web-app/src/haapi-stepper/feature/actions/client-operation/client-operations.ts Removed monolithic client-operations implementation (replaced by per-operation modules).
src/login-web-app/src/haapi-stepper/data-access/types/haapi-action.types.ts Refines WebAuthn registration typing and introduces selected-option enum.

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

Copy link
Copy Markdown
Contributor

Copilot AI commented May 5, 2026

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • download.cypress.io
    • Triggering command: /usr/local/bin/node node dist/index.js --exec install (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

aleixsuau and others added 3 commits May 5, 2026 11:54
Extract `createMockExternalBrowserFlowAction`, `createMockBankIdAction`,
`createMockWebAuthnRegistrationAction`,
`createMockWebAuthnAnyDeviceBothOptionsAction`, and
`createMockWebAuthnPlatformOnlyAnyDeviceAction` (plus their default-title
constants) from the spec into the shared `util/tests/mocks.ts`, so future
specs can reuse them. The spec drops 114 lines of local helpers and
relies on imports.

`stubPublicKeyCredential` stays in the spec — it's a global-API stub
builder, not a HAAPI-data factory.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Consolidate the four WebAuthn tests (enables, disables on API absent,
disables on missing platform authenticator, splits into two buttons)
into one flat `describe('WebAuthn')` block. Each test sets up only what
it needs; a single `afterEach` cleans up `vi.stubGlobal` and the mocked
`useIsWebAuthnPlatformAuthenticatorAvailable` hook.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The JSDoc examples in `HaapiStepper.tsx` and `HaapiStepperSelectorUI.tsx`
referenced fields that don't exist on `dataHelpers` (`formActions`,
`selectorActions`, `clientOperationActions`). Updated to the real
`actions.{form,selector,clientOperation}` shape so consumers can
copy/paste them.

Also swapped the example `key` props from `action.kind` / `link.rel`
(neither unique) to `action.id` / `link.id`, matching the production
factory in `defaultHaapiStepperActionElementFactory.tsx`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@aleixsuau aleixsuau marked this pull request as ready for review May 11, 2026 08:39
Copy link
Copy Markdown
Contributor

@luisgoncalves luisgoncalves left a comment

Choose a reason for hiding this comment

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

The approach in this PR is a nice trick, and I think we can stick with it for now.

However, I'm starting to wonder if we should treat each client operation as a specific thing, having it's own component. Almost as if we "flattened" client operations to the top level, having form, selector, webauthn, bankid.

The reason I'm saying this is twofold:

  1. The UI for a client operation can be very specific and it always requires some corresponding "backing logic". We currently have this logic inner in the code, instead of being already split from the component needing it.
  2. Client operations are an extension point, so, although unlikely, custom plugins could define new ones. In this case it doesn't make sense to show any UI by default, as we can't know what the operation means.

Splitting would probably allow removing a smell that we currently have in HaapiStepperClientOperationUI - showBankIdSessionTimeLeft, i.e. BankID-specifics.

Anyway, lets discuss this separately, if you think it's worth it.

--

I also smoke tested locally and it looks good 👍

@aleixsuau
Copy link
Copy Markdown
Contributor Author

The approach in this PR is a nice trick, and I think we can stick with it for now.

However, I'm starting to wonder if we should treat each client operation as a specific thing, having it's own component. Almost as if we "flattened" client operations to the top level, having form, selector, webauthn, bankid.

The reason I'm saying this is twofold:

  1. The UI for a client operation can be very specific and it always requires some corresponding "backing logic". We currently have this logic inner in the code, instead of being already split from the component needing it.
  2. Client operations are an extension point, so, although unlikely, custom plugins could define new ones. In this case it doesn't make sense to show any UI by default, as we can't know what the operation means.

Splitting would probably allow removing a smell that we currently have in HaapiStepperClientOperationUI - showBankIdSessionTimeLeft, i.e. BankID-specifics.

Anyway, lets discuss this separately, if you think it's worth it.

--

I also smoke tested locally and it looks good 👍

I see what you mean, but I don't think it is an issue at this point:

1 - HaapiStepperClientOperationUI's responsibility is to provide a default view that allows performing any client operation. It currently covers 3 options (bankid, webAuthn, and external browser flow), and to me, it makes sense that it accepts configuration options related to any of them. It also facilitates creating custom UI steps because the consumer does not need to think about implementation details.

If the default UI doesn't match, the consumer can always use the clientOperationActionRenderInterceptor.

That said, I don't see any arm in creating HaapiStepperBankIDClientOperationUI, HaapiStepperWebAuthnClientOperationUI, and HaapiStepperEBFClientOperationUI, and make HaapiStepperClientOperationUI just switch UI component according to the client operation type. WDYT?

2 - This seems an important point because we are throwing if the client operation is not one of the allowed options (bankid, webAuthn, and external browser flow). Should we handle this case as you said? It seems not showing any UI would break the current contract 🤔

@luisgoncalves
Copy link
Copy Markdown
Contributor

@aleixsuau

1 - HaapiStepperClientOperationUI's responsibility is to provide a default view that allows performing any client operation. It currently covers 3 options (bankid, webAuthn, and external browser flow), and to me, it makes sense that it accepts configuration options related to any of them. It also facilitates creating custom UI steps because the consumer does not need to think about implementation details.

I don't think it makes so much sense, but this isn't very important either.

just switch UI component according to the client operation type

It would allow removing the trick in this PR (flattening client operation), because the component for "webauthn-registration" would know how to display the different options. If we also do that, I agree with splitting and I think it would be a good thing. If not, than Idk if we need to do it now.

2 - This seems an important point because we are throwing if the client operation is not one of the allowed options

You're right, not showing anything (and not throwing) would most likely result (silently) in a weird UI. So, lets leave it as is. Interceptor can be used for this.

@aleixsuau
Copy link
Copy Markdown
Contributor Author

aleixsuau commented May 22, 2026

@aleixsuau

1 - HaapiStepperClientOperationUI's responsibility is to provide a default view that allows performing any client operation. It currently covers 3 options (bankid, webAuthn, and external browser flow), and to me, it makes sense that it accepts configuration options related to any of them. It also facilitates creating custom UI steps because the consumer does not need to think about implementation details.

I don't think it makes so much sense, but this isn't very important either.

just switch UI component according to the client operation type

It would allow removing the trick in this PR (flattening client operation), because the component for "webauthn-registration" would know how to display the different options. If we also do that, I agree with splitting and I think it would be a good thing. If not, than Idk if we need to do it now.

2 - This seems an important point because we are throwing if the client operation is not one of the allowed options

You're right, not showing anything (and not throwing) would most likely result (silently) in a weird UI. So, lets leave it as is. Interceptor can be used for this.

The WebAuth Client Operation kind of "breaks" the contract because it might provide 2 "actions" the user needs to choose from (like a selector), while the rest of the client operations and form actions provide/represent 1. This seems a data "issue" to me, which is why I solved it at the data level.

aleixsuau and others added 5 commits May 22, 2026 16:07
…ion.origin

`new URL(action.model.arguments.href)` throws if `href` is ever relative.
The server normally sends an absolute URL for external-browser-flow, but
parsing with `window.location.origin` as the base keeps the runner safe
against malformed payloads and makes test setups simpler. Suggested by
Luis on Copilot review of PR #154.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The any-device-mode split was labelling the two buttons "This device"
and "Another device", which reads as a phone/laptop distinction. The
real distinction is between the platform authenticator (built-in
biometric) and a cross-platform authenticator (FIDO2 security key) —
matching the Velocity templates. Suggested by Luis on PR #154.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The action factory was keying form and selector elements by
`action.model.href` and `action.title` respectively — neither is
guaranteed unique. Switch both to `action.id` (the canonical unique
identifier, already used for client-operation), matching what Luis
suggested on PR #154.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ests

Brings the feature branch up to date with
`integration/IS-5161/login-web-app` (new viewnames system, step-element
factories, isQrCodeLink utility, HaapiStepperStepUI refactor, README +
spec updates).

Also fixes a test-environment regression where `mocks.ts` and
`bankid/open-bankid-app.ts` were importing from the `data-access`
barrel, which transitively loads `bootstrap-configuration` and throws
`'Configuration not set'` when `window.__CONFIG__` is unset. Switched
both to deep imports from `data-access/types/...`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@aleixsuau aleixsuau force-pushed the feature/IS-11252/webauthn-client-operations branch from ca19214 to 3abff38 Compare May 22, 2026 15:01
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@aleixsuau aleixsuau merged commit 1130068 into integration/IS-5161/login-web-app May 22, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants