Skip to main content

Passkey Sign-In

Passkeys are a modern, secure way for users to sign in using biometric authentication (Face ID, Touch ID) or device PIN. They work across devices when synced via iCloud (iOS) or Google Password Manager (Android).

Platform Support​

iOS​

  • Minimum: iOS 16+
  • Uses native AuthenticationServices framework
  • Presents native passkey creation/selection UI
  • Full integration with iCloud Keychain
  • Supports Face ID, Touch ID, and device PIN
  • Check PASSKEYS_SUPPORTED constant before using

Android​

  • Minimum: Android 9.0+ (API 28+)
  • Uses Credential Manager API with passkey support
  • Presents native passkey creation/selection UI
  • Integrates with Google Password Manager
  • Supports biometric and device authentication
  • Check PASSKEYS_SUPPORTED constant before using

Setup Requirements​

iOS Configuration​

1. Add Associated Domains Capability​

To use passkeys on iOS, you must add the Associated Domains capability to your app:

  1. Open your Xcode project
  2. Select your target and go to Signing & Capabilities tab
  3. Click + Capability button
  4. Search for and add Associated Domains
  5. In the Associated Domains section, add your domain in this format:
    webcredentials:yourdomain.com

With Expo, use the config plugin instead: "webCredentialsDomains": ["yourdomain.com"] adds webcredentials:yourdomain.com (merged with your existing associated domains).

📚 Reference: See Apple's guide on adding capabilities to your app for detailed instructions.

2. Configure your Domain​

Your backend domain must host an apple-app-site-association file at:

https://yourdomain.com/.well-known/apple-app-site-association

This file tells Apple that your app is allowed to use passkeys from your domain:

{
"webcredentials": {
"apps": [
"TEAM_ID.BUNDLE_ID"
]
}
}

Replace:

  • TEAM_ID with your Apple Team ID
  • BUNDLE_ID with your app's bundle identifier

3. Use the Correct Domain in Your Code​

When creating or asserting passkeys, the rp.id (relying party ID) must match your configured domain:

const options = {
requestJson: JSON.stringify({
challenge: 'your-challenge',
rp: {
id: 'yourdomain.com', // Must match your Associated Domain
name: 'Your App Name'
},
user: {
id: 'user-id',
name: 'user@yourdomain.com',
displayName: 'User Name'
},
pubKeyCredParams: [{ type: 'public-key', alg: -7 }],
timeout: 60000,
attestation: 'none',
authenticatorSelection: {
authenticatorAttachment: 'platform',
userVerification: 'preferred'
}
})
};

Android Configuration​

Android 9+ uses the Credential Manager API. Nothing to add to the app, but the rp.id domain must declare your app with a Digital Asset Links file at:

https://yourdomain.com/.well-known/assetlinks.json
[
{
"relation": [
"delegate_permission/common.handle_all_urls",
"delegate_permission/common.get_login_creds"
],
"target": {
"namespace": "android_app",
"package_name": "com.yourcompany.yourapp",
"sha256_cert_fingerprints": [
"AA:BB:CC:..."
]
}
}
]

Replace:

  • package_name with your applicationId
  • sha256_cert_fingerprints with the SHA-256 of every signing key: debug, release and the Play App Signing key (Play Console > Setup > App signing). cd android && ./gradlew signingReport prints the debug and release ones

Without it Credential Manager refuses the request for that rp.id and the method rejects. Your backend must also validate the passkey responses.

Methods​

createPassKey(options: PasskeyOptions): Promise<PassKeyCredentialResult>​

Creates a new passkey credential.

Parameters​

NameTypeDescription
optionsPasskeyOptionsPasskey creation configuration

Returns​

Promise<PassKeyCredentialResult>

PassKeyCredentialResult:

{
type: CredentialsType.PASSKEY;
data: string; // JSON string containing the public key credential response
}

Example Usage​

import { createPassKey, PASSKEYS_SUPPORTED, CredentialsType } from 'react-native-oauth-essentials';

async function registerPasskey(): Promise<void> {
if (!PASSKEYS_SUPPORTED) {
console.log('Passkeys are not supported on this platform.');
return;
}

try {
const credential = await createPassKey({
requestJson: JSON.stringify({
challenge: await getServerChallenge(),
rp: {
id: 'yourdomain.com',
name: 'Your App'
},
user: {
id: 'user-id',
name: 'user@yourdomain.com',
displayName: 'User Display Name'
},
pubKeyCredParams: [{ type: 'public-key', alg: -7 }],
timeout: 60000,
attestation: 'none',
authenticatorSelection: {
authenticatorAttachment: 'platform',
userVerification: 'preferred'
}
})
});

if (credential.type === CredentialsType.PASSKEY) {
const passkeyData = JSON.parse(credential.data);
console.log('Passkey created successfully');

// Send to your backend for verification
await verifyPasskeyWithBackend(passkeyData);
}
} catch (error) {
console.error('Failed to create passkey:', error);
}
}

getPassKey(options: PasskeyOptions): Promise<PassKeyCredentialResult>​

Retrieves an existing passkey credential for authentication.

Parameters​

NameTypeDescription
optionsPasskeyOptionsPasskey retrieval configuration

Returns​

Promise<PassKeyCredentialResult>

Example Usage​

import { getPassKey, PASSKEYS_SUPPORTED, CredentialsType } from 'react-native-oauth-essentials';

async function authenticateWithPasskey(): Promise<void> {
if (!PASSKEYS_SUPPORTED) {
console.log('Passkeys are not supported on this platform.');
return;
}

try {
const credential = await getPassKey({
requestJson: JSON.stringify({
challenge: await getServerChallenge(),
rpId: 'yourdomain.com',
timeout: 60000,
userVerification: 'preferred'
})
});

if (credential.type === CredentialsType.PASSKEY) {
const passkeyData = JSON.parse(credential.data);
console.log('Passkey authentication successful');

// Send to your backend for verification
await verifyPasskeyWithBackend(passkeyData);
}
} catch (error) {
console.error('Failed to authenticate with passkey:', error);
}
}

Passkey AutoFill (iOS 16+)​

Pass mediation: 'autofill' to show the user's passkeys in the QuickType bar of the username field instead of a modal sheet. Start the request when the sign in screen mounts; the promise only settles when the user picks a passkey or the request is cancelled. The username TextInput must have textContentType="username" and autoComplete="username".

import { useEffect } from 'react';
import { TextInput } from 'react-native';
import { getPassKey, cancelPassKeyAutoFill, CredentialsType } from 'react-native-oauth-essentials';

function SignInScreen() {
useEffect(() => {
getPassKey({ requestJson: JSON.stringify(requestOptions), mediation: 'autofill' })
.then((credential) => {
if (credential.type === CredentialsType.PASSKEY) {
verifyPasskeyWithBackend(JSON.parse(credential.data));
}
})
.catch(console.error);
return () => {
cancelPassKeyAutoFill();
};
}, []);

return <TextInput textContentType="username" autoComplete="username" />;
}

A modal getPassKey or createPassKey call cancels the pending AutoFill request, which resolves as CANCELLED. On Android mediation is ignored and cancelPassKeyAutoFill resolves false.

Best Practices​

  1. Always check PASSKEYS_SUPPORTED before calling passkey methods
  2. Use the correct domain in rp.id - it must match your Associated Domain (iOS)
  3. Store challenges securely on your backend and never reuse them
  4. Validate attestations on your backend to ensure passkey authenticity
  5. Handle errors gracefully - users may cancel or lack passkey support
  6. Use biometric verification - passkeys are secure because they require user verification
  7. Test thoroughly - test on physical devices with real passkeys

Domain Configuration Summary​

PlatformRequirementFormatExample
iOSAdd Associated Domain capabilitywebcredentials:domain.comwebcredentials:myapp.com
iOSHost apple-app-site-associationAt /.well-known/apple-app-site-associationSee iOS Configuration section
AndroidHost assetlinks.jsonAt /.well-known/assetlinks.jsonSee Android Configuration section
BothUse domain in passkey optionsIn rp.id or rpId fieldyourdomain.com

Platform Differences​

FeatureiOSAndroid
Minimum VersioniOS 16+Android 9.0+ (API 28+)
Domain SetupRequired (Associated Domain capability + apple-app-site-association)Required (assetlinks.json)
UINative iOS modalNative Android bottom sheet
StorageiCloud KeychainGoogle Password Manager
SyncAcross Apple devicesAcross Google account devices