Whether you use the raw browser APIs or one of our helpers you’ll have flexibility to set your own registration and authentication options. This page provides an overview and some recommendations related to these options.
challenge
timeout
rp
rp
options is an object with 2 fields: id
and name
.
rp.id
(aka RPID) should be your app top-level domain. For example, if your app is hosted on https://your.app.xyz
the RPID should be app.xyz
unless you have good reasons to do otherwise (see below).
Reasons to set RPID to specific sub-domains
rp.id
, or RPID, is a way to identify the website a passkey is associated with. Once set at registration time, it determines the set of origins on which the passkey may be be used. The WebAuthn spec states that the RPID must be a “registrable domain suffix of, or equal to” the current domain. If the page creating a passkey is hosted at https://your.app.xyz
, the RPID can thus be “your.app.xyz” or “app.xyz”.A passkey with RPID “your.app.xyz” cannot be used on https://www.app.xyz
or https://foo.app.xyz
. However a passkey created with RPID “app.xyz” will be usable on all https://*.app.xyz
sub-domains: https://your.app.xyz
, https://www.app.xyz
, https://foo.app.xyz
, and so on. Hence our general recommendation above to set app.xyz
(top-level domain) as the RPID to maximize flexibility.A reason why you might want to set the RPID to “your.app.xyz” instead of “app.xyz” like recommended above is extra security: if you are worried about user passkeys being usable across all your sub-domains, it makes sense to scope passkeys to the sub-domain they’re meant to be used on, and only that sub-domain.If you scope passkeys to a specific sub-domain, be aware that migrating your app to a different sub-domain later will require a migration process where users have to re-enroll themselves by creating new passkeys on the new sub-domain. Passkeys cannot be transferred from one RPID to another.rp.id
will show up in the initial registration popup:
rp.name
doesn’t show up in the popup so can be set to anything. We recommend setting it to the correctly capitalized name of your app, in case browsers start showing it in their native UIs in the future.
attestation
pubKeyCredParams
and alg
pubKeyCredParams
is a list of supported algorithms. If you’re relying on Turnkey to validate passkey signatures, this list should be: [{alg: -7, type: "public-key"}, {alg: -257, type: "public-key"}]
.
The integers -7
and -257
are algorithm identifiers for ES256 (aka P256) and RS256 (aka RSA), respectively. The full list of possible values is part of the COSE standard, maintained by IANA. Currently Turnkey only supports ES256 and RS256.
user
user
field has three sub-fields:
id
: also known as “user handle”, isn’t visible to the end-user. We strongly recommend setting this to a random value (e.g. const id = new Uint8Array(32); crypto.getRandomValues(id)
) to make sure a new passkey is created. Be aware: if you accidentally set this value to an existing user handle, the corresponding passkey will be overridden! This section of spec is clear on the matter: “the user handle ought not be a constant value across different accounts, even for non-discoverable credentials”.
name
: this will show up in the passkey list modal (see screenshot below). We recommend setting this to something the user will recognize: their email, the name of your app, or potentially leave this up to the user:
displayName
: as far as we can tell this doesn’t show up in current browser UIs. It might show up in future iterations so it’s best to populate this with the same value as name
.authenticatorSelection
authenticatorAttachment
Empty (default) | platform | cross-platform |
---|---|---|
If you want broad compatibility, leave this option empty, and the browser UI will allow for both internal and external passkeys. | If set to platform , only internal authenticators (face ID, touch ID, and so on) can be registered. | If set to cross-platform , only passkeys from other devices or attached via USB are allowed. |
![]() | ![]() | ![]() |
requireResidentKey
and residentKey
residentKey
is discouraged
and requireResidentKey
is false
.
Important note: the default for requireResidentKey
(discouraged
) results in different outcomes based on OS: Android devices create non-discoverable credentials whereas iOS devices create discoverable credentials. If you want to create discoverable credentials whenever possible, set requireResidentKey
to false
and residentKey
to preferred
, which work across Android and iOS devices.
userVerification
discouraged
: yubikey PINs won’t be required even if the device technically supports it. We’ve found that for TouchID/FaceID, authentication will still be required however.preferred
: yubikey PINs and other authentication mechanisms will be required if supported, but devices without them will be accepted.required
: authenticators without user verification support won’t be accepted.userVerification
to “discouraged” or “preferred” because some authenticators do not support user verification.
Due to poor yubikey PIN UX in browsers, setting userVerification
to “discouraged” is best unless you operate with a strict security threat model where user verification makes a big difference.
“preferred” is the default value if you don’t specify this option.
challenge
rpId
rp.id
option during passkey registration. Passkeys are domain bound, so it’s not possible to use a passkey registered with rp.id
set to “foo.com” and use it on “bar.com”. This is a core anti-phishing counter-measure.
allowCredentials
transports
list is optional but results in better, more targeted prompts. For example, here are screenshot of targeted prompts captured on Chrome, on a MacBook laptop:
transports: ["internal"] | transports: ["usb"] | transports: ["hybrid"] |
---|---|---|
![]() | ![]() | ![]() |
Buffer.from(storedCredentialId, "base64")
) to avoid issues.
If the wrong credential ID is specified, transports: ["internal"]
is set, browsers error right away because they can enumerate internal credentials. Chrome, for example, displays the following error:
transports
set (or with other-than-internal transports
set), browsers won’t error right away because they can’t enumerate external credentials. They will display an error once the user has pressed their security key or gone through the cross-device passkey flow:
attestation
attestation
above.
timeout
timeout
above.
UserVerification
UserVerification
above.