FIDO2 Touch Prompt UI (Issue #2631)
Implemented for the auth.webauthn.enabled path in app/webauthn/touchPrompt.js. FIDO2 / WebAuthn support is an opt-in beta behind auth.webauthn.enabled (see ADR-021).
Date: 2026-06-09 Issue: #2631 - FIDO2: missing UI prompt when waiting for authenticator touch Status: Implemented (treated as an enhancement, not a regression)
Summary
#2631 reports that while the authenticator waits for a physical touch (the user-presence check), Teams for Linux shows no on-screen prompt, so the app appears frozen. This is a missing-UX gap in the opt-in FIDO2 beta, not a regression: authentication completes correctly if the user knows to touch the key. Adding a "touch your security key now" prompt is feasible and low-risk, with one honest limitation, namely that we can show the prompt for the duration of the security-key call but cannot pinpoint the exact instant the key starts waiting for touch.
Current behaviour
A security-key assertion from the Microsoft login page flows through:
app/browser/tools/webauthnOverride.jscallsipcRenderer.invoke("webauthn:get", …).app/webauthn/index.js:80handleWebauthnRequest: whenuserVerification === "required", the PIN is collected up front viaapp/webauthn/pinDialog.js. This PIN dialog is the only UI the flow ever shows.app/webauthn/index.js:114-115callsfido2Backend.getAssertion/createCredential.app/webauthn/fido2Backend.js:45spawnsfido2-assert/fido2-cred. After the PIN is written to stdin (fido2Backend.js:76), the child process performs the user-presence check and blocks until the key is physically touched. Theawaitdoes not resolve untilproc.on("close")(fido2Backend.js:80).
During that touch wait the PIN dialog has already closed and no other window is shown. A search confirms there is no touch / user-presence UI anywhere in app/webauthn/.
Why this is a feature request, not a bug
The FIDO2 beta (PR #2357, ADR-021) deliberately scoped its UI to PIN entry. A touch prompt was never built. Nothing broke; the flow works for a user who knows to touch the key. The honest framing is a missing-UX polish step in an opt-in beta.
Feasibility and approach
Feasible and low-risk. The cleanest insertion point is handleWebauthnRequest in app/webauthn/index.js, wrapping the backend call at lines 114-115: show a non-blocking, frameless BrowserWindow (or a Notification) immediately before the await, and dismiss it in a finally once the promise settles, whether on success, error, or the existing 60s timeout (fido2Backend.js:54). This reuses the BrowserWindow pattern already proven in pinDialog.js, so no new infrastructure is needed, and because the backend already resolves or rejects on completion and timeout, dismiss-on-completion is essentially free.
Update (2026-07-29): since this note was written, app/_shared/securePrompt.js has been extracted and is used by the smartcard PIN flow (app/clientCertificate/index.js, see ADR 024). app/webauthn/pinDialog.js still has its own BrowserWindow implementation, so either base works, but building the touch prompt on the shared helper is preferable to adding a third prompt window implementation.
The one limitation we should not over-promise
fido2-assert / fido2-cred emit the Enter PIN for text to stderr (fido2Backend.js:72) but emit nothing at the user-presence step, so the touch wait is silent at the CLI boundary. We can reliably show a prompt that spans the security-key call (or starts right after the PIN is written, or when the process spawns for no-PIN flows), but we cannot fire it at the precise moment the key's LED starts blinking. The realistic deliverable is a generic "waiting for your security key, touch it now" prompt, not a touch-instant signal. Localised libfido2 builds also translate the stderr text (an ADR-021 known limitation), a further reason to avoid stderr-driven precision and prefer wrapping the whole call.
Risks
- Cancellation: a "Cancel" button would have to replicate the detached-process-group kill already used on timeout (
fido2Backend.js:59) and reject cleanly, or it risks orphaning thefido2child. The simplest first cut is an informational prompt with no cancel, auto-dismissed on completion or timeout. - Seamlessness: the PIN dialog closes before the touch wait, so the new prompt should appear without a visible gap.
- Window lifecycle: the
finallythat dismisses the prompt should checkisDestroyed()first, since the parent window could be closed during the wait (up to the 60s timeout) and dismissing an already-destroyed window would throw. - Scope: Linux-only, behind
auth.webauthn.enabled, beta.
What shipped
The BrowserWindow-around-the-await approach above, with one deviation: it ships with a Cancel
button rather than the informational-only v1 this note recommended, because
#2631 was later widened to ask for
"a cancel that rejects". The orphaned-child risk the Risks section flagged is handled by giving
spawnFido2 an AbortSignal that reuses the very same detached-process-group kill as the timeout,
so a cancel cannot leave a fido2 child holding the device.
Validated on real hardware by @spthiel:
the prompt appears with no visible gap after the PIN dialog closes, touching the key completes the
sign-in, Cancel and the 60s timeout both land on Microsoft's "We couldn't sign you in" page, and no
fido2-assert process is left behind.
Still open: the auth.webauthn.enabled off path, where Electron's native WebAuthn draws no UI
on Linux at all. That needs interception for users who did not opt into the beta, and it is not
obvious what Cancel should mean when the underlying Chromium call cannot be aborted.
Recommendation
Worth doing as a small enhancement. Reframe the ticket from bug to enhancement, set expectations to a call-duration prompt rather than touch-instant, and treat it as the next polish step after the PIN-entry UI the beta already shipped. A focused spike on the BrowserWindow-around-the-await approach, with no cancel in v1, is the lowest-risk path.