On October 19, 2020, Apple posted an explanation of their take on WebAuthn: https://webkit.org/blog/11312/meet-face-id-and-touch-id-for-the-web/
FaceID and TouchID are supported starting with iOS 14 and macOS Big Sur. To make it work, for every allowCredentials entry that is a platform authenticator you need to add transport: ["internal"]
, otherwise it will continue prompting for a USB/NFC token.
In essence, this:
const publicKeyCredentialRequestOptions = {
challenge: challengeBuffer,
allowCredentials: [{
id: 'credentialsIdentifierOne', // Windows Hello
type: 'public-key'
}, {
id: 'credentialsIdentifierTwo', // iOS FaceID
type: 'public-key'
}],
timeout: 60000
}
Becomes this:
const publicKeyCredentialRequestOptions = {
challenge: challengeBuffer,
allowCredentials: [{
id: 'credentialsIdentifierOne', // Windows Hello
type: 'public-key',
transports: ["internal"]
}, {
id: 'credentialsIdentifierTwo', // iOS FaceID
type: 'public-key',
transports: ["internal"]
}],
timeout: 60000
}
Overall, this is in line with the specs and you should be able to retrieve the list of transports for an authenticator by calling .getTransports()
on AuthenticatorResponse
. This is not widely documented in tutorials and even less widely supported, so be wary:
When setting up cross-platform authenticators, the returned value will typically only contain the transport that was used and not all transports that the authenticator supports. For example, YubiKey 5 NFC will only return ["usb"]
when used while plugged in.
So you should either avoid setting transports
for these authenticators all together, or set them to all "non-internal" transports: ["usb", "nfc", "ble"]
.
As for calling .getTransports()
, check if it is available (December 2020, Safari still doesn't have it), and if it is not, then fallback to appropriate transports
based on whenever you are requesting a platform authenticator or a cross-platform authenticator.
Another very important point: if you are launching SFSafariWebView to handle this in your app, be it PWA, Cordova or native, it will unexpectedly fail. Reason? Apple decided that user activation (click/tap) is required before FaceID is even offered as an option to the user. What's worse is that you have no idea that the authentication is guaranteed to fail - iOS makes it seem like everything is running correctly and even prompts the user to plug in their authenticator, while knowing that no such authenticator exists (credentials ID points to FaceID that iOS knows of).
The solution to above problem is to add an extra step and interrupt the authentication flow by asking the user to press a button before invoking WebAuthn code.