Skip to content

fix(idp): passkey login degrades on WebView/insecure contexts instead of crashing — closes #556#558

Merged
melvincarvalho merged 1 commit into
gh-pagesfrom
issue-556-webauthn-degrade
Jun 11, 2026
Merged

fix(idp): passkey login degrades on WebView/insecure contexts instead of crashing — closes #556#558
melvincarvalho merged 1 commit into
gh-pagesfrom
issue-556-webauthn-degrade

Conversation

@melvincarvalho

Copy link
Copy Markdown
Contributor

Closes #556. Port-back from jspod#65, but JSS can fix it better than jspod could.

The two problems on stale WebViews (de-Googled phones)

  1. crypto.randomUUID crash. loginPage's inline JS built a visitorId with crypto.randomUUID() — absent on Chromium <92 / non-secure contexts → Sign in with Passkey threw crypto.randomUUID is not a function.
  2. WebAuthn-unavailable crash. Even past that, both ceremonies (navigator.credentials.get/create) fail with a cryptic error on a WebView that can't do WebAuthn.

Why JSS's fix is cleaner than jspod's polyfill

jspod had to polyfill randomUUID because the call lives inside the solid-oidc library it imports. JSS owns the call site. And the call was redundant: authenticationOptions (passkey.js:226) already mints the challengeKey via Node's always-available crypto.randomUUID() and returns it (:236), and the client already echoes options.challengeKey on verify (views.js:298). So the browser call generated a value the server immediately overrode.

Fix 1: drop the client crypto.randomUUID(); send {}. Server mints the key, client echoes it — functionally identical, zero browser crypto.

Fix 2: an up-front isSecureContext + PublicKeyCredential + credentials.get/create guard on both loginWithPasskey and registerPasskey that steers the user to the password form (login) or "Skip for now" (prompt) — both already on the page. This addresses jspod#65's own caveat: the polyfill only fixes randomUUID, never the WebAuthn/secure-context gate, which is the real wall. We can't make passkeys work on those WebViews (nobody can), but the page now degrades cleanly to password/Schnorr login instead of erroring.

Tests

test/passkey-webauthn-degrade.test.js (5 — the first direct passkey-endpoint coverage, noted missing when #78 closed):

  • no crypto.randomUUID() call survives in the served JS; empty options body is sent
  • both pages carry the secure-context/WebAuthn guard
  • every inline <script> parses (apostrophe-escaping regression guard)
  • functional proof: the options endpoint returns a usable challengeKey for an empty body — so dropping the client call is complete, not a regression

Full suite: 958/958.

Scope note

Server-side crypto.randomUUID in keys.js/accounts.js/credentials.js/passkey.js is Node (always available) — untouched. views.js:273 was the only browser-side site.

… of crashing (#556)

Stale Android System WebViews (de-Googled phones, #46/#556 arc) lack
crypto.randomUUID and a secure context. Two problems, two fixes:

1. The login page's inline JS called `crypto.randomUUID()` to build a
   `visitorId` — missing on those WebViews, so 'Sign in with Passkey'
   threw 'crypto.randomUUID is not a function'. Unlike jspod#65 (where
   the call lives inside the imported solid-oidc library, forcing a
   polyfill), JSS OWNS the call site: authenticationOptions already
   mints the challengeKey via Node crypto (always available) and
   returns it, and the client already echoes options.challengeKey on
   verify. The browser call was pure redundancy — removed; the options
   request now sends {}.

2. Both passkey ceremonies would otherwise fail deep with a cryptic
   WebAuthn error on a WebView that can't do WebAuthn at all. Added an
   up-front secure-context + PublicKeyCredential + credentials.get/
   create guard to loginWithPasskey and registerPasskey that steers
   the user to the password form (login) or 'Skip for now' (prompt) —
   both already on the page. Goes beyond jspod's polyfill, which could
   only stop the randomUUID crash, not the WebAuthn-unavailable one.

Tests (test/passkey-webauthn-degrade.test.js, 5): no randomUUID() call
survives in the served JS, empty options body is sent, both pages
carry the guard, every inline script parses (escaping regression
guard), and — the functional proof — the options endpoint returns a
usable challengeKey for an EMPTY body, so dropping the client call is
complete, not a regression. First direct passkey-endpoint coverage
(noted missing when #78 closed). 958/958 passing.

Closes #556.

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

Pull request overview

This PR improves the IdP passkey UX on stale Android WebViews and insecure contexts by avoiding browser-only crypto APIs and guarding WebAuthn calls so the login / registration flows degrade gracefully instead of crashing.

Changes:

  • Remove the client-side crypto.randomUUID() usage from the passkey login options request and rely on the server-minted challengeKey.
  • Add upfront secure-context + WebAuthn availability guards to both passkey login and passkey registration UI flows.
  • Add a new test suite verifying the rendered HTML/JS behavior and that the options endpoint works with an empty request body.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/idp/views.js Removes browser crypto.randomUUID() call and adds WebAuthn/secure-context guards to degrade to password/skip paths.
test/passkey-webauthn-degrade.test.js Adds regression coverage for the degraded behavior, inline script parsability, and server-side challengeKey minting with empty body.

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

@melvincarvalho
melvincarvalho merged commit 8af19c4 into gh-pages Jun 11, 2026
2 checks passed
@melvincarvalho
melvincarvalho deleted the issue-556-webauthn-degrade branch June 11, 2026 05:46
melvincarvalho added a commit that referenced this pull request Jun 11, 2026
Eight PRs merged since 0.0.206 — IdP hardening, protocol conformance,
and the de-Googled-phone (#46) arc:

IdP / auth
- #558 passkey login degrades cleanly on stale WebViews / insecure
  contexts instead of crashing — drops a redundant browser
  crypto.randomUUID and guards both ceremonies on secure-context +
  WebAuthn (closes #556)
- #554 IdP login-form error now survives the POST→redirect→GET cycle
  (rode in a non-persisted field that Interaction.save() dropped);
  failed sign-ins show the error instead of a silent re-render
  (closes #514)
- #551 RFC 9207 'iss' authorization-response param normalized to match
  the discovery issuer, so strict OIDC clients (solid-oidc) complete
  sign-in (closes #524)

Tunnel
- #555 opt-in, per-tunnel credential passthrough (Cookie /
  Authorization / Set-Cookie) so authenticated access works through a
  tunnel; the relay's own IdP session cookies are isolated from the
  tunnel client (closes #530)

Content negotiation / git
- #553 HEAD now mirrors GET's negotiated Content-Type / Content-Length
  / Cache-Control for files (RFC 9110 §9.3.2 parity) (closes #552)
- #550 git WAC preHandler 401/402/403 responses carry the git CORS
  headers, so browser git clients see the status, not a CORS error
  (closes #548)
- #549 first HTTP-contract coverage for the git handler + fixes a
  DATA_ROOT test-pollution bug (closes #375)

Docs / metadata
- #547 README tagline + npm keywords surface the agentic positioning
  (closes #406)
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.

idp: login page's passkey JS calls crypto.randomUUID — crashes on stale Android WebViews (de-Googled phones)

2 participants