- About
- Documentation
- Requirements
- Quickstart
- Building and Running Sample App
- Project Structure
- Code Structure
- Troubleshooting
Twilio Passkeys SDK enables developers to easily add Passkeys into their existing authentication flows within their own mobile applications. The Verify Passkeys SDK supports passkeys creation and authentication using the FIDO/WebAuthn industry standard.
Verify Passkeys Overview WebAuthn
- Android Studio for Android development. Minimum version Hedgehog
- Xcode for iOS development. 16.x
- IntelliJ IDEA or Android Studio for shared code development.
- Android 9 (API Level 28) or higher
- iOS 16 or higher
- Gradle 8.2
- Java 17
- Add the implementation app build gradle:
implementation("com.twilio:twilio-verify-passkeys-android:$sdkVersionName")
- Sync the project.
- Use the SDK by creating an instance of TwilioPasskey:
val twilioPasskey = TwilioPasskey(context)
- In Xcode project go to File > Add Packages
- Enter the repository URL
https://github.com/twilio/twilio-verify-passkeys-ios
- Select the version you'd like to use (preferably the latest release)
- Click Add Package to finish integrating the SDK
- Import TwilioPasskeys in the files you will make use of it:
let twilioPasskey = TwilioPasskey()
- Add the implementation statement in dependencies:
implementation("com.twilio:twilio-verify-passkeys-common:$sdkVersionName")
- Sync the project.
- To use the passkeys SDK in KMP there are separate initialization for iOS and Android as the SDK needs the Android context to work:
- iOS
val twilioPasskey = TwilioPasskey()
- Android
val twilioPasskey = TwilioPasskey(context)
- iOS
Use the TwilioPasskey
instance to create a registration by calling the create(String, AppContext)
function.
The first param is a String
representation of a create passkey request, check how to create passkey payload (createPayload
).
The second param is an instance of a com.twilio.passkeys.AppContext
, it is created by passing the current Activity
instance in Android or the UIWindow
instance in iOS.
You can also call the create(CreatePasskeyRequest, AppContext)
function, where CreatePasskeyRequest
is a wrapper object of a create passkey payload schema.
Android
val createPasskeyResult = twilioPasskey.create(createPasskeyRequest, AppContext(activity))
when(createPasskeyResult) {
is CreatePasskeyResult.Success -> {
// verify the createPasskeyResult.createPasskeyResponse against your backend and finish sign up
}
is CreatePasskeyResult.Error -> {
// handle error
}
}
iOS
let response = try await twilioPasskey.create(createPasskeyRequest: createPasskeyRequest, appContext: AppContext(uiWindow: window))
if let success = response as? CreatePasskeyResult.Success {
// verify the createPasskeyResult.createPasskeyResponse against your backend and finish sign up
} else if let error = response as? CreatePasskeyResult.Error {
// handle error
}
Use the TwilioPasskey
instance to authenticate a user by calling the authenticate(String, AppContext)
function.
The first param is a String
representation of an authentication request, it follows the schema of an authenticate passkey payload (authenticatePayload
).
The second param is an instance of a com.twilio.passkeys.AppContext
, it is created by passing the current Activity
instance in Android or the UIWindow
instance in iOS.
You can also call the authenticate(AuthenticatePasskeyRequest, AppContext)
function, which the AuthenticatePasskeyRequest
is a wrapper object of an authenticate passkey payload.
Android
val authenticatePasskeyResult = twilioPasskey.authenticate(authenticatePasskeyRequest, AppContext(activity))
when(authenticatePasskeyResult) {
is AuthenticatePasskeyResult.Success -> {
// verify the authenticatePasskeyResult.authenticatePasskeyResponse against your backend
}
is AuthenticatePasskeyResult.Error -> {
// handle error
}
}
iOS
let response = try await twilioPasskey.authenticate(authenticatePasskeyRequest: authenticatePasskeyRequest, appContext: AppContext(uiWindow: window))
if let success = response as? AuthenticatePasskeyResult.Success {
// verify the authenticatePasskeyResult.authenticatePasskeyResponse against your backend and finish sign in.
} else if let error = response as? AuthenticatePasskeyResult.Error {
// handle error
}
The creation payload for creating a passkey is a String obtained by requesting your backend a challenge for registering a user, it uses the JSON schema:
{
"rp": {
"id": "your_backend",
"name": "PasskeySample"
},
"user": {
"id": "WUV...5Ng",
"name": "1234567890",
"displayName": "1234567890"
},
"challenge": "WUY...jZQ",
"pubKeyCredParams": [
{
"type": "public-key",
"alg": -7
}
],
"timeout": 600000,
"excludeCredentials": [],
"authenticatorSelection": {
"authenticatorAttachment": "platform",
"residentKey": "preferred",
"userVerification": "preferred"
}
}
- rp (Relying Party - Server Details)
-
id
- The domain where the passkey will be used (e.g.,
example.com
). - Must match the domain where the authentication will happen.
- The domain where the passkey will be used (e.g.,
-
name
- A human-readable name for the service (shown to the user during registration).
- user (User Information)
-
id
- A Base64 URL-encoded unique user ID (usually a server-generated random value).
- Used to identify the user uniquely in the system.
-
name
- A username or account identifier.
- Could be an email, phone number, or user ID.
-
displayName
- A user-friendly name displayed during registration.
- Typically the same as
name
, but could be more descriptive.
- challenge (Server-Generated Random Value)
- A Base64 URL-encoded random value generated by the server.
- Prevents replay attacks by ensuring each request is unique.
- The client (browser/device) signs this challenge when authenticating.
- pubKeyCredParams (Public Key Parameters)
-
type
- Indicates the credential being created is a public-private key pair.
- Always
"public-key"
for WebAuthn and passkeys to work.
-
alg
- Represents
ES256
(Elliptic Curve Digital Signature Algorithm - ECDSA using SHA-256). - Other possible values include
-257
for RSA.
- Represents
- timeout
- Specifies how long the user has to complete the registration.
- excludeCredentials (Prevent Duplicate Registrations)
- An empty array means the system allows registering new passkeys.
- If filled, prevents the user from registering a new credential if they already have one matching the specified credential IDs.
- authenticatorSelection
-
authenticatorAttachment
platform
→ Uses a built-in authenticator (e.g., Google Credential Manager, iCloud Keychain, 1Password, etc). The credential is stored on the device and can only be used there.cross-platform
→ Uses a roaming authenticator (e.g., security keys like YubiKey, external FIDO2 devices). These credentials can be used across multiple devices.
-
residentKey
required
→ Guarantees the credential is discoverable (stored on the authenticator). If the authenticator does not support discoverable credentials, registration will fail.preferred
→ Requests a discoverable credential if the authenticator supports it, but if not, a non-discoverable credential may be created instead.discouraged
→ Creates a non-discoverable credential, meaning the relying party (RP) must store and provide the credential ID for authentication.
-
userVerification
required
→ The authenticator must verify the user (e.g., fingerprint, face recognition, PIN). If the authenticator does not support user verification, registration/authentication will fail.preferred
→ The authenticator will attempt user verification if supported (e.g., biometric authentication). If not, it may still proceed without verification.discouraged
→ User verification is not required. The credential can be used without any local authentication, making it more convenient but less secure.
The authenticate payload for authenticating a user is a JSON with the schema:
{
"publicKey": {
"challenge": "WUM...2Mw",
"timeout": 300000,
"rpId": "your_backend",
"allowCredentials": [],
"userVerification": "preferred"
}
}
- challenge (Server-Generated Random Value)
- A Base64 URL-encoded random value generated by the server.
- Prevents replay attacks by ensuring each request is unique.
- The client (browser/device) signs this challenge when authenticating.
- timeout
- Timeout in milliseconds (e.g.,
300000 ms = 5 minutes
). - Defines how long the client has to complete the authentication before it expires.
- rpId (Relying Party ID)
- The domain of the service requesting authentication.
- Must match the domain for which the passkey was originally created.
- Example: If a passkey was registered for
example.com
, this value must be"example.com"
.
- allowCredentials
- An empty array means the client can use any available passkey for this domain.
- If specific credentials (public keys) are listed here, the client will be restricted to only those.
- This is useful for allowing specific passkeys if a user has multiple.
- userVerification
- Specifies whether the system should require user verification (e.g., biometrics or PIN).
required
→ The authenticator must verify the user (e.g., fingerprint, face recognition, PIN). If the authenticator does not support user verification, authentication will fail.preferred
→ The authenticator will attempt user verification if supported (e.g., biometric authentication). If not, it may still proceed without verification.discouraged
→ User verification is not required. The credential can be used without any local authentication, making it more convenient but less secure.
- Clone the repository
git clone https://github.com/twilio/twilio-verify-passkeys.git
- Open the project
- Use IntelliJ IDEA or Android Studio.
- Configure the backend URL
- Update the
BaseUrl
ingradle.properties
.
- Update the
- Build and run
- Select the
androidApp
module and run the app.
- Select the
In order to create passkeys you need to create the association for the sample app to communicate to the RP.
- Add support for digital asset links. You can generate a sha256 by running
./gradlew signingreport
and copying the sha256 of the keystore used to build the app. - Replace value with sha256.
- To sync passkeys with Web browsers you should replace https://github.com/twilio-labs/function-templates/blob/main/passkeys-backend/assets/.well-know/assetlinks.json#L9 with the website url e.g. https://passkeys.twil.io Check Android association
- Add the origin, following the official documentation.
For accurate passkey testing, ensure the following on your emulator or physical device:
- Google Account: Sign in with a Google account.
- Google Play Services: Confirm that Google Play services are enabled.
- Device Security: Set up a screen lock with security measures and enable biometric authentication (e.g., fingerprint or face recognition).
To enable passkeys on the backend you can follow the detailed guide. Then you should also:
- Verify Digital Asset Links support
- Ensure you have added support for digital asset links and your backend includes an entry with the SHA-256 signature of your app.
- Generate the SHA-256 fingerprint using:
./gradlew signingReport
- Add the origin
- If not already configured, follow the official documentation to add the origin.
Passkey Creation | Passkey Authentication |
---|---|
![]() |
![]() |
- Clone the repository
git clone https://github.com/twilio/twilio-verify-passkeys.git
- Open the project
- Use IntelliJ IDEA or Android Studio or open
iosApp
module in Xcode.
- Use IntelliJ IDEA or Android Studio or open
- Configure your backend
- Set your backend domain and entitlements. Update these values in Constants.swift and iosApp.entitlements
- Constants.swift - Define your backend domain:
let domain: String = "passkeys-service.com" // Example domain; replace with your backend domain
- iosApp.entitlements - Specify entitlements:
<string>webcredentials:passkeys-service.com</string> <!-- Example entitlement; update with your domain -->
- Set your backend domain and entitlements. Update these values in Constants.swift and iosApp.entitlements
- Build and run
- Select the
iosApp
module and run the app.
- Select the
In order to create passkeys you need to create the association for the sample app to communicate to the RP.
For accurate passkey testing, ensure the following on your physical device:
- Apple ID: Ensure the device is signed in with an Apple ID.
- iCloud Keychain: Confirm that iCloud Keychain is enabled.
- Device Security: Set up a passcode and enable Face ID or Touch ID for biometric authentication.
For simulators:
- Use Xcode 14+ with iOS 16+ simulators for the latest passkey support.
- Go to “Features” > “Face ID” or “Touch ID” > “Enrolled”.
- Trigger biometric events with “Matching Touch” or “Non-Matching Touch”.
Passkey Creation | Passkey Authentication |
---|---|
![]() |
![]() |
shared
: This module contains the shared code, including business logic, data models, and utility functions.androidApp
: This module is specific to the Android platform and includes the Android sample app code.iosApp
: This module is specific to the iOS platform and includes the iOS sample app code.
The shared
module contains code shared between Android and iOS. This includes:
- Data models
- Business logic
- Utility functions
The androidApp
module contains Android-specific code, such as:
- Sample application that works as a code snippet for integrating with the Twilio Verify Passkeys SDK
- Android-specific UI components
The iosApp
module contains iOS-specific code, such as:
- Sample application that works as a code snippet for integrating with the Twilio Verify Passkeys SDK
- iOS-specific UI components
This section provides guidance on resolving common issues encountered when using the Android and iOS SDKs. The SDK may throw various exceptions to indicate specific errors. Below are troubleshooting steps for each exception type.
Handles errors related to WebAuthn DOM configurations for passkeys.
-
Android:
- Verify that the
/.well-known/assetlinks.json
file is correctly set up on your server. - Ensure the file has the proper syntax and contains the correct SHA256 certificate fingerprints.
- Reference the Android Guide for setup details.
- Verify that the
-
iOS:
- Confirm that the
/.well-known/apple-app-site-association
(AASA) file is correctly configured. - Check that the file is served with the correct MIME type (
application/json
without extension). - Check Apple CDN Cache: The AASA file is cached by Apple’s CDN, and updates may not reflect immediately.
- Visit:
https://app-site-association.cdn-apple.com/a/v1/<your-domain>
to verify the cached version.
- Visit:
- Note: The CDN should refresh upon app installation, but delays can occur.
- More info: Apple Documentation
- Refer to the Apple Guide for detailed instructions.
- Confirm that the
Thrown when the user voluntarily cancels an operation.
- This exception is intentional and indicates user action.
- Provide clear UI feedback to inform users of the cancellation.
- Offer an option to retry the operation if necessary.
Thrown when an operation is interrupted.
- Check for background processes or app lifecycle events that may be interrupting the operation.
- Retry the operation in a controlled manner.
- Ensure that proper exception handling is implemented in coroutine or thread-based operations.
Indicates the device does not support or has disabled the passkeys feature.
- Confirm that the device supports WebAuthn and passkeys.
- Android/iOS: Ensure that biometric or device credentials are enabled.
- Web: Check for browser compatibility with WebAuthn.
Thrown when no passkey credentials are available.
- Verify that the user has registered passkey credentials on the device.
- Prompt the user to create a passkey if none are available.
- Ensure the app correctly queries for existing credentials.
Raised when the attestation object is null
.
- Verify that the iOS device supports passkey attestation.
- Ensure that attestation is enabled in the authentication request.
- Update to the latest iOS version if compatibility issues persist.
Thrown when the JSON payload is invalid.
- Validate the JSON payload structure against the expected schema.
- Use JSON validation tools to identify formatting errors.
- Ensure that all required fields are included and correctly formatted.
Represents an unspecified error.
- Check the error message and stack trace for additional context.
- Ensure the SDK is up-to-date.
- If the issue persists:
- Enable detailed logging.
- Contact support with logs for further investigation.
- Keep the SDK and dependencies updated to the latest version.
- Refer to platform-specific SDK documentation for detailed integration steps.
- Use logging extensively to capture error details during development and production.