Hybrid Transport and Cross-Device Passkeys

Hybrid transport — the CTAP2 mechanism formerly called caBLE — lets a user sign in on a laptop using a passkey that lives on their phone, brokered by a QR code and a Bluetooth proximity check. It is why a passkey held only on an iPhone can authenticate a session in desktop Chrome. This page explains the handshake, the transports hint that surfaces it, the UX to present, and the proximity security model that keeps it phishing-resistant. It sits under platform vs roaming authenticator trade-offs.


Mechanism Reference

Element Role
QR code (on the target device) Carries a one-time key-agreement handshake
Bluetooth Low Energy Proximity proof — the phone must be physically near the target
Tunnel service Relays encrypted CTAP2 messages between phone and target
transports: ['hybrid'] The credential hint that a passkey may be used cross-device
authenticatorAttachment: 'cross-platform' The phone answers a get() as a roaming authenticator

The QR and BLE steps together prevent remote relay: an attacker who phishes the QR cannot complete the ceremony without also being in Bluetooth range, so the origin-binding guarantee is preserved even across devices.


Architecture and Data Flow

Hybrid transport cross-device flow Target device displays a QR code; the phone scans it and confirms Bluetooth proximity; the encrypted assertion tunnels back to the target and on to the relying party. Laptop (target) shows QR Phone (authenticator) UV + sign Relying Party scan QR tunnel assertion BLE proximity check laptop forwards assertion → RP verify

Root Cause Analysis (of cross-device failures)

1. No hybrid transport hint stored. If you saved a credential without transports, some clients will not surface the cross-device option, so a phone-only passkey appears unusable on desktop.

2. Bluetooth disabled or unsupported. Hybrid requires BLE on both devices; a desktop without Bluetooth cannot complete proximity, and the ceremony stalls or times out.

3. Treating hybrid as a fallback, not a first-class path. Hiding “use another device” behind an error confuses users who legitimately keep their passkey on a phone.


Step-by-Step Resolution

Step 1 — Persist transports at registration

const transports = credential.response.getTransports?.() ?? []; // e.g. ['hybrid','internal']
await saveCredential({ credentialId, transports, aaguid });

Step 2 — Echo transports in allowCredentials

allowCredentials: creds.map((c) => ({
  id: c.credentialId,
  type: 'public-key',
  transports: c.transports, // include 'hybrid' so the client offers cross-device
})),

Step 3 — Present cross-device as a deliberate choice

Offer “Use a passkey on another device” in the modal picker (the browser renders the QR), not only after a local failure.

Step 4 — Feature-detect and message BLE

If a local ceremony fails and the platform lacks Bluetooth, message the user that cross-device needs Bluetooth on both devices rather than showing a generic error — see fixing NotAllowedError.


Verification and Testing

Cross-device flows are hard to fully emulate, but you can verify the plumbing:

// Assert transports are captured and echoed
expect(saved.transports).toContain('hybrid');
expect(options.allowCredentials[0].transports).toContain('hybrid');

End-to-end, test on real hardware: register a passkey on a phone, then sign in on a desktop and confirm the QR + Bluetooth flow completes. Confirm that a desktop with Bluetooth off surfaces a specific “enable Bluetooth” message, not a generic failure.


Pitfalls

1. Dropping the transports hint. Store and echo ['hybrid', …] or cross-device may not appear.

2. Hiding cross-device behind an error. Present it as a normal option for phone-resident passkeys.

3. Assuming relay is possible remotely. The BLE proximity check is intentional; do not attempt to bypass it — it is what preserves phishing resistance.


Related