Platforms
Configure iOS Associated Domains and Android Digital Asset Links
Native passkeys require a link between your app and your relying-party domain. In WebAuthn, the relying-party ID (RP ID) scopes a passkey. A credential created for one RP ID only works for that RP ID, so registration, authentication, server verification, and native app association must agree.
For most apps, use the domain users recognize as your sign-in domain, such as example.com. Use a subdomain like login.example.com only when credentials should be limited to that subdomain. Do not use a URL, path, port, or API host just because your options endpoint lives there.
Use the same RP ID domain in these places:
- The Expo plugin config.
- Registration options as
rp.id. - Authentication options as
rpId. - Server verification as the expected RP ID.
- The platform association file hosted under
/.well-known.
For these examples, the domain is example.com.
The included apps/example-backend can serve the iOS and Android association files for the example app. Configure its trust environment variables when the app identifiers or signing fingerprints differ from the committed demo values.
Expo config
The plugin writes webcredentials:example.com to iOS Associated Domains. Android passkey association is verified from the hosted Digital Asset Links file, so the plugin does not add Android intent filters or make your app handle every https://example.com URL.
{
"expo": {
"ios": {
"bundleIdentifier": "com.example.app"
},
"android": {
"package": "com.example.app"
},
"plugins": [
[
"expo-easy-passkey",
{
"domains": ["example.com"]
}
]
]
}
}iOS
Host an Apple App Site Association file at:
https://example.com/.well-known/apple-app-site-associationIt must be served as JSON without a .json extension in the URL.
{
"webcredentials": {
"apps": ["ABCDE12345.com.example.app"]
}
}ABCDE12345 is your Apple Team ID. com.example.app is your iOS bundle identifier.
Android
Host Digital Asset Links at:
https://example.com/.well-known/assetlinks.jsonThe package name and SHA-256 certificate fingerprint must match the build installed on the device.
[
{
"relation": [
"delegate_permission/common.get_login_creds",
"delegate_permission/common.handle_all_urls"
],
"target": {
"namespace": "android_app",
"package_name": "com.example.app",
"sha256_cert_fingerprints": [
"12:34:56:78:90:AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90:AB:CD:EF"
]
}
}
]Android passkey verification currently expects both delegate_permission/common.get_login_creds and delegate_permission/common.handle_all_urls in assetlinks.json. If you also want the app to open links for this domain, configure Android App Links separately in your Expo config with the path constraints your app owns.
Debug, preview, and production builds usually have different signing certificates. Add each fingerprint you plan to test.
Credential Manager derives the WebAuthn origin from the installed APK's signing certificate; app JavaScript does not choose it. Convert the 32 raw SHA-256 fingerprint bytes to unpadded base64url and verify the resulting exact origin:
android:apk-key-hash:<unpadded-base64url-certificate-fingerprint>The example backend performs this conversion for every comma-separated ANDROID_SHA256_CERT_FINGERPRINTS value and combines the results with its HTTPS origin. Keep this allowlist in trusted server configuration. Never add an origin from ceremony request data.
WebAuthn options
Your registration options should use the same relying-party domain:
const registrationOptions = {
challenge,
rp: {
id: "example.com",
name: "Example",
},
user,
...
};Authentication uses the same value as rpId:
const authenticationOptions = {
challenge,
rpId: "example.com",
...
};Use the domain only. rp.id and rpId must not include https://, a path, or a port.
The RP ID is separate from origin validation. On Android, rp.id remains the associated domain while the verified origin is the APK-key-hash value derived by Credential Manager.
Timeout hint
Registration and authentication options accept WebAuthn timeout in milliseconds on every platform. Treat it as a hint, not a guaranteed cancel deadline:
- Android forwards the value into Credential Manager public-key JSON when it is present.
- iOS AuthenticationServices accepts the field on the native request objects but does not apply a consumer timeout.
Omit timeout when you do not have a server-supplied value. Do not strip it on iOS just because the platform ignores it.
iOS registration policy
iOS registration uses AuthenticationServices platform passkeys only. Unsupported relying-party constraints fail with ERR_PASSKEY_VALIDATION before the system passkey UI appears.
Reachable behavior:
attestationvaluesnone,indirect,direct, andenterpriseare mapped onto the AuthenticationServices registration request.- Registration proceeds only when
pubKeyCredParamsis empty or includes ES256 (alg: -7). Empty parameter lists use the platform default, which remains ES256. authenticatorSelection.authenticatorAttachmentmay be omitted or set toplatform.cross-platformis rejected until security-key registration is implemented.- Platform passkeys are always discoverable.
residentKeyvaluespreferredandrequired, and legacyrequireResidentKeytrue or false, are accepted as that fixed semantics.residentKey: "discouraged"is rejected as incompatible.
Web and unsupported runtimes
Web, server rendering, and other non-native runtimes can import the package safely. Capability detection returns { supported: false, platform: "web" } on web. Registration and authentication reject with ERR_PASSKEY_UNSUPPORTED.
Browser WebAuthn ceremonies are intentionally out of scope. Treat web support as a documented unsupported fallback until a future release adds browser ceremonies.
Expo Go also cannot load this native module. Import and capability checks succeed; ceremony methods reject with ERR_PASSKEY_UNSUPPORTED and tell you to use a development build or production build.
Device testing
Final passkey testing needs real devices. iOS Simulator and Android emulator behavior can still differ from physical devices for biometrics and some credential-provider edge cases.
For Android emulator development, sign in with a Google account and set a screen lock (PIN, pattern, or password) before creating or asserting passkeys. Credential Manager needs that account (or another enabled passkey-capable password manager); without it registration often fails with No create options available.
When something fails, check association first. A bad AASA file, stale Android fingerprint, or mismatched rp.id can stop the system passkey UI before your JavaScript receives a useful response. For Android registration, also verify that the device or emulator has a secure screen lock and an enabled credential provider with an account that can save passkeys.