Skip to content

feat: upgrade WebAuthnKit to v3.0.0#24

Open
elukewalker wants to merge 23 commits into
YubicoLabs:mainfrom
elukewalker:feat/v3.0.0-upgrade
Open

feat: upgrade WebAuthnKit to v3.0.0#24
elukewalker wants to merge 23 commits into
YubicoLabs:mainfrom
elukewalker:feat/v3.0.0-upgrade

Conversation

@elukewalker

Copy link
Copy Markdown
Contributor

Summary

This PR upgrades WebAuthnKit to v3.0.0, bringing the project up to date with modern AWS runtimes, the latest Yubico webauthn-server-core library, passkey autofill support, and a full BDD test suite.

Backend (Java Lambda)

  • Upgrade webauthn-server-core 2.0.0 → 2.9.0 with custom Gson TypeAdapters (ByteArrayTypeAdapter, InstantTypeAdapter, OptionalTypeAdapterFactory) for Java 17 module system compatibility
  • Upgrade Lambda runtime java8.al2 → java17
  • Remove FIDO Metadata Service (MDS) integration (no longer required)
  • Add parseResidentKey() for backward-compatible resident key handling
  • Fix AssertionRequestStorage and RDSRegistrationStorage for 2.x API changes

Node.js Lambdas

  • Upgrade all Lambda runtimes nodejs16.x → nodejs20.x
  • Migrate all Node.js Lambdas from AWS SDK v2 → v3 (@aws-sdk/client-*)
  • Migrate Aurora Serverless v1 → v2 (data-api-client@aws-sdk/client-rds-data)
  • Fix double-parse handling for Gson-serialized Lambda responses

React Frontend

  • Add passkey autofill (conditional mediation) via new /passkey route and PasskeyLoginPage component
  • Add autoComplete="username webauthn" hint to login input for browser passkey suggestions
  • Fix credentials === {} object comparison bug in HomePage

Infrastructure & Deployment

  • Update Dockerfile: Ubuntu 22.04 + OpenJDK 17 + Node 20 + Maven 3.9 + SAM CLI + AWS CLI v2
  • Migrate CreateDBSchemaFunctionCaller inline Lambda to AWS SDK v3
  • Use presigned S3 URL for Amplify deployment (removes bucket policy requirement)

Testing

  • Add BDD test suite: 24 Cucumber.js + Playwright scenarios covering registration, authentication, passkey autofill, recovery codes, credential management, server-verified PIN, and account deletion
  • All unit tests pass (4/4 Java, all Node)

Test plan

  • Docker image builds: docker build --platform linux/amd64 -t starterkit:dev scripts/Mac-Linux/
  • Java Lambda unit tests pass: mvn test in backend/lambda-functions/JavaWebAuthnLib/
  • Full stack deploys via ./deployStarterKit.sh
  • BDD suite passes: npx cucumber-js (requires deployed stack + virtual authenticator browser)
  • Passkey autofill flow works on /passkey route in Chrome/Safari

Breaking changes

  • Java runtime bumped to 17 — existing stacks must be redeployed
  • Node runtime bumped to 20 — existing stacks must be redeployed
  • MDS integration removed

🤖 Generated with Claude Code

elukewalker and others added 23 commits June 9, 2026 16:29
- Upgrade webauthn-server-core 2.0.0 → 2.9.0 with custom Gson TypeAdapters
  (ByteArray, Instant, Optional) for Java 17 module system compatibility
- Upgrade Lambda runtimes: java8.al2 → java17, nodejs16.x → nodejs20.x
- Migrate all Node.js Lambdas from AWS SDK v2 to v3
- Add passkey autofill (conditional mediation) via new /passkey route
- Add autoComplete="username webauthn" hint to login page
- Remove FIDO Metadata Service (MDS) integration
- Add parseResidentKey() for backward-compatible resident key handling
- Fix credentials === {} object comparison bug in HomePage
- Add BDD test suite: 24 Cucumber.js + Playwright scenarios
- Bump all versions to 3.0.0, add CHANGELOG.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace openkbs/jdk-mvn-py3 base image with ubuntu:22.04
- Install OpenJDK 17, Node.js 20, Maven 3.9, SAM CLI, AWS CLI v2
- Mount ~/.aws/sso in deploy script for SSO credential support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- backend/template.yaml: replace deprecated EngineMode:serverless with
  EngineMode:provisioned + ServerlessV2ScalingConfiguration + db.serverless
  instance resource (Aurora Serverless v1 retired by AWS, new clusters
  unavailable)
- scripts/Mac-Linux/deployStarterKit.sh: remove :ro from SSO cache volume
  mount so SAM CLI can write temporary token files during sam deploy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
aws-sdk v2 is not bundled in nodejs20.x Lambda runtime, causing the
CFN custom resource to fail with 'Cannot find module aws-sdk' and never
respond to CloudFormation. Migrated to @aws-sdk/client-lambda (v3).
Also increased Timeout from 20s to 60s to allow schema creation time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cy requirement

aws amplify start-deployment with s3:// source URL requires the Amplify
service to have a bucket policy granting it access. Private buckets fail
silently with UnauthorizedException. Generate a 1-hour presigned URL
instead — no bucket policy needed, works with default private buckets.

Also removed &> /dev/null suppression so deployment errors are visible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
FIDO2KitAPI/DatabaseController.js and CreateAuth/CreateAuthChallengeFIDO2.js
both required a local './db-client' file that does not exist. Should use
the 'data-api-client' npm package already listed in package.json deps.
VerifyAuth and CreateDBSchema were already using the correct import.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All four Node Lambda functions (FIDO2KitAPI, CreateAuth, VerifyAuth,
CreateDBSchema) use the data-api-client npm package for RDS Data API
access but several were missing it from their declared dependencies,
causing Runtime.ImportModuleError on Node 20.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
data-api-client v1 internally requires aws-sdk v2, which is not bundled
in the Node 20 Lambda runtime. Replace with a local db-client.js shim
in each Lambda function that wraps @aws-sdk/client-rds-data (bundled in
Node 20) and exposes the identical .query(sql, params) interface.

No call-site changes needed in DatabaseController.js or other callers —
the shim is a drop-in replacement.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Deploy script:
- Remove > /dev/null 2>&1 from mvn, SAM, and npm build steps so errors
  surface instead of silently failing and deploying broken artifacts

Registration flow:
- Move defaultData inside IdentifierFirstSignUpFlow component so
  localStorage.getItem() runs at render time, not module load
- InitUserStep navigates directly to '/' after auth tokens are set,
  skipping the now-unnecessary RegisterKeySuccessStep click
- Guarantee a digit in the random Cognito password to satisfy
  RequireNumbers policy

Credential display:
- Guard credential.lastUsedTime null check in Credential.tsx
- Use optional chaining + fallback for registrationRequest.requireResidentKey
  → residentKey rename in RegistrationRequest (v3.0.0 upgrade)

BDD tests:
- h3 → h5 selector for "Security Keys" heading (dashboard uses h5)
- Update credential locator to match actual DOM (button, not ul/li/a)
- I click Cancel step now falls back to span.btn-link "Log In" on /register

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- credentials.steps.js: replace ul/li/anchor credential selectors with
  .card button:has-text("Edit") to match actual div-based credential rendering
- credentials.steps.js: fix h3->h5 heading and add recovery codes modal
  dismissal in 'I am on the dashboard' step
- common.steps.js: fix sign-out selector (a->button, Logout->Sign Out)
  and dashboard greeting (h1->h2, Hi->Welcome)
- recovery.steps.js: fix sign-out selector and close-button text ("Not now")
- recovery-exhaustion.steps.js: fix sign-out selector and close-button text
- server-verified-pin.steps.js: fix sign-out selector, modal title
  ("U2F Password" not "Enter Server-Verified PIN"), and save button
  ("Submit" not "OK")
- add-credential.steps.js: handle AddCredential modal with no nickname
  input field; fix credential count check to use button selector
- helpers.js: fix signOutUser, captureRecoveryCodes, dismissRecoveryCodesModal
  to use correct button text
… tests

"Not now, ask me again later" only hides the modal in React state so it
re-opens on the next getAll() reload, intercepting pointer events on
other modals (Delete, Change PIN). "Ignore, and don't ask again" sets
localStorage which prevents re-open for the browser session.

Also adds Recovery Codes modal dismissal to the 'I am signed in' step
in server-verified-pin.steps.js so the Change PIN modal is unobstructed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…condition

Using isVisible() immediately after navigation misses the modal if React
hasn't rendered it yet. Use Promise.race to wait for either the modal or
the credential list before deciding whether to dismiss. Affects I am on the
dashboard, I am signed in (svp), recovery-exhaustion loop, and
captureRecoveryCodes (now waits for hidden state before returning).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…austion loop

Two root-cause fixes:
1. Recovery Codes modal fires in a useEffect AFTER credentials load, so
   waitForDashboardReady must wait for creds first, then wait up to 4s for
   the modal to appear before checking/dismissing. This eliminates the race
   on account-deletion where the modal appeared after waitForDashboardReady
   already returned.
2. recovery-exhaustion loop must use "Not now" (not "Ignore") so localStorage
   is never set — the exhaustion warning modal fires only when
   allRecoveryCodesUsed === true AND ignoreModal === false. Using Ignore
   permanently suppresses the modal, breaking the exhaustion assertion.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…overyCodes

Two root-cause fixes:
1. account-deletion: DeleteUser.tsx renders the Delete button in Modal.Footer
   (not Modal.Body). The selector .modal-body button:has-text("Delete") never
   matched; changed to .modal-footer button:has-text("Delete").
2. recovery-exhaustion: captureRecoveryCodes was clicking Ignore which sets
   localStorage recoveryCodesModal=true, making ignoreModal=true for the
   rest of the browser session. RecoveryCodes useEffect gate is:
   (!recoveryCodesViewed || allRecoveryCodesUsed) && !ignoreModal — so
   the exhaustion modal never fired. Restored Not now (no localStorage write).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the AddCredential modal submits without a nickname field (the field
was removed in v3.0.0), FIDO2KitAPI.js passed undefined to the Java Lambda
which threw a NullPointerException (HTTP 500). Default to 'Security Key'.

Also update package-lock.json version to 3.0.0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
JavaWebAuthnLib uses BuildMethod: makefile in the SAM template.
The makefile build method invokes make to copy the pre-built JAR
into SAM's ARTIFACTS_DIR. Without make in the container, sam build
fails with 'Path resolution for runtime: provided of binary: make
was not successful'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sam deploy --s3-bucket handles artifact packaging inline.
The standalone sam package step was missing --s3-bucket and --region,
causing it to fail. It was also discarding its output to /dev/null
so the packaged template was never used. Removing it entirely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Platform passkeys were being categorized as security keys because
FIDO2KitAPI.js was dropping requireAuthenticatorAttachment before
forwarding to the Java Lambda. App.java now reads this field and
sets AuthenticatorSelectionCriteria.authenticatorAttachment so the
backend stores PLATFORM in the credential, allowing HomePage.tsx
to correctly categorize it as a trusted device.
HomePage.tsx checked authenticatorAttachment === "PLATFORM" but the
Java library serializes it as "platform" (lowercase), so trusted
devices always appeared as security keys. Changed to case-insensitive
comparison.

WebAuthnClient.ts called requestUV(challengeResponse) in the UV=false
path, but AddTrustedDevice passes null for requestUV. Added a null
guard with a descriptive error instead of a TypeError crash.
In webauthn-server-core 2.x, requireResidentKey (boolean) was
replaced by residentKey (enum string). EditTrustedDevice was calling
.toString() on the old field without optional chaining, crashing
with TypeError after a trusted device was added. Applied the same
safe pattern already used in EditCredential.
Commit 246f25b (Cody Salas) removed the nickname input from
AddTrustedDevice but left no smart default, causing all credentials
to fall back to "Security Key". Now that requireAuthenticatorAttachment
is forwarded, use it to select the right default nickname.
@elukewalker elukewalker requested a review from dmennis June 23, 2026 17:45
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.

1 participant