Unit 42 built the first RDP client outside Windows to support WebAuthn redirection, beating Microsoft's own macOS, iOS, and Linux clients to that capability (FreeRDP has since followed). No browser API could do what they needed, the protocol spec was missing entire commands, and Microsoft's Windows implementation routes through internal, undocumented code paths that had to be reverse-engineered with IDA Pro.
The Spec Was Missing Commands
[MS-RDPEWA] defines a dynamic virtual channel (DVC) called Microsoft::Windows::RDP.Webauthn with four CBOR commands: Command 5 (MakeCredential or GetAssertion), 6 (IUVPAA – platform authenticator query), 7 (CANCEL), and 8 (API_VERSION). Clean. Straightforward. Except when Unit 42 started wiring it up, they discovered the spec stopped there. The March 2026 revision 3.0 later added two more commands – GetCredentials (9) and GetAuthenticatorList (12) – gated to Windows 11 24H2+ and Server 2025+ via KB5065789. But at the time, the spec was incomplete, leaving the engineers to fill in the gaps.
The Browser API Was Useless
The obvious plan: receive the WebAuthn request from the server, call navigator.credentials.create() in the extension, send back the response. Here's the killer: when a user visits okta.com inside the remote session and triggers WebAuthn, the server intercepts the ceremony. It computes clientDataHash = SHA-256(clientDataJSON) where clientDataJSON contains the page's origin, challenge, and ceremony type. Then it sends that 32-byte hash over the RDP channel. On the client side, navigator.credentials.create() insists on constructing its own clientDataJSON with its own origin (chrome-extension://..., not https://okta.com). It hashes that, the authenticator signs over it, and when the assertion comes back to Okta: hash mismatch. SHA-256 is a one-way function. No going around that. Reconstructing the clientDataJSON from known challenge and origin fails because browsers don't produce byte-identical JSON, native apps use SDKs that add variability, and older Windows servers don't even send the ingredients – only the 32-byte hash.
Building a Custom Browser API
No existing browser API can accept a pre-computed clientDataHash and pass it directly to an authenticator. Not navigator.credentials. Not chrome.webAuthenticationProxy (designed for the opposite direction). Not remoteDesktopClientOverride (requires the original JSON). Not WebHID (USB-only, no Touch ID, no phone-as-authenticator). So Unit 42 built one: a custom extension API with makeCredential() and getAssertion() methods identical to navigator.credentials in every way except the caller supplies the clientDataHash directly. The W3C WebAuthn working group has since started standardizing exactly this case (the remoteClientDataJSON extension, editor's draft section 10.1.6), but it's not yet in any shipping browser.
What started as a two-week project turned into a deep dive through disassembly with IDA Pro, an AI-assisted MCP bridge that cut days off reverse-engineering, and a custom API that now fills a real gap. The direction is encouraging: the spec is being updated, and browser vendors are likely to follow.
Source: How We Added WebAuthn to a Browser-Based RDP Client
Domain: unit42.paloaltonetworks.com
Comments load interactively on the live page.