Get your free and exclusive +30-page Authentication Analytics Whitepaper

Native Passkey Challenges: iOS & Android Pitfalls

Native passkey implementation is 100x harder than web. Discover every iOS AASA CDN trap, Android signing key mistake and WebView pitfall before you ship.

Vincent Delitz

Vincent

Created: February 19, 2026

Updated: March 3, 2026

native ios android passkey implementation challenges

1. Introduction: why your "2-week sprint" will turn into 6 months#

"Just add passkeys to our mobile app."

Seven words that have launched countless debugging sessions. Your PM might have seen a passkey iOS demo somewhere: tap Face ID, user is logged in. The pitch writes itself: no passwords involved, better conversion rates, phishing-resistant MFA. A dream for frictionless onboarding and login in iOS and Android apps.

However, you as a developer then have to implement it.

BuyVsBuildGuide Icon

Want to know whether to buy or build your passkey solution? Get our 60-page Buy vs. Build Guide (incl. cost breakdown & adoption strategies). Trusted by leading enterprises.

Get Free Guide

Passkey implementation is complex on every platform. The initial integration might look relatively straightforward, but the real work starts after launch:

  • debugging edge cases across browsers,
  • handling device and OS inconsistencies,
  • dealing with third-party password manager interference and
  • driving passkey adoption.

Read our article on 5 passkey issues that surface post-launch for more details.

On native mobile apps, you get all of those post-launch problems plus months of platform-specific setup before you even reach launch. You could debug why passkeys work on iPhone 15 but not iPhone 14, why certain Android OEMs behave differently from your Pixel test device and why Apple's CDN cached your old config for 48 hours. You will discover that "Universal Links" are not universal, that Android has different signing keys and that a single misplaced character in a JSON file will break authentication for your entire user base - silently.

This post documents the native-specific pitfalls. It is based on our experience shipping native passkey integrations and the patterns we see across customer deployments.

2. Domain Association: where everything breaks first#

Web apps have it easy. The browser sees example.com, trusts example.com. Done.

Native apps have no URL. They are just code on a phone. So Apple and Google invented file-based trust systems that map your app to your domain. Different files, different formats, different failure modes. When they fail - and they most likely will - they fail silently.

2.1 iOS: the AASA file#

Apple's trust system requires you to host a JSON file at https://yourdomain.com/.well-known/apple-app-site-association:

{ "webcredentials": { "apps": ["TEAMID.com.example.app"] } }

Looks trivial. It is not.

Silent failure modes:

  • Wrong Team ID (e.g. switched from personal to corporate Apple Developer account): silent failure, no error
  • Wrong Bundle ID: silent failure
  • Typo in either field: silent failure

The error message is very generic. You don't get detailed hints.

2.2 iOS: AASA CDN caching#

iOS does not fetch your AASA file directly. It goes through Apple's CDN at app-site-association.cdn-apple.com. You update your file, deploy it, verify it loads in your browser and passkeys still do not work because Apple's CDN cached the old version.

Apple's CDN imposes its own caching behavior regardless of your origin headers. Community investigations show that observed max-age values vary (commonly around 3600 seconds, but not guaranteed) and propagation delays are unpredictable. There is no programmatic cache purge.

You can inspect what Apple's CDN currently serves (and its cache headers):

https://app-site-association.cdn-apple.com/a/v1/{yourdomain.com}

Here's a developer bypass (for testing only):

  1. Add ?mode=developer to your Associated Domains entitlement
  2. Enable Developer Mode on your test device
  3. Turn on "Associated Domains Development" in Settings
  4. The device now fetches the AASA file directly from your server

Miss any step and you are back to the cached version. Remove the ?mode=developer flag before App Store submission or your app gets rejected.

2.3 iOS: AASA HTTP requirements#

Apple's crawler is strict about the HTTP response:

  • Content-Type must be application/json (for unsigned files) or application/pkcs7-mime (for signed files). Adding charset=utf-8 works in practice, but serving it as text/json or application/octet-stream will break. See Apple's Associated Domains documentation for the accepted formats.
  • No redirects. Your nginx adds a trailing slash redirect? Broken. CloudFront redirecting to S3? Broken.
  • HTTPS only with a valid certificate.
  • The server must be reachable over IPv4. Apple's CDN has had issues with IPv6-only servers.

2.4 iOS: post-install verification delay#

About 5% of users who try to use passkeys immediately after installing your app will hit an Application is not associated with this domain error. The reason: iOS has not yet verified the AASA file for the freshly installed app. iOS 18+ improved this by returning a retryable error, but on older versions the failure is silent. Build retry logic or delay the passkey prompt after first install.

2.5 Android: signing key confusion#

Android's equivalent is assetlinks.json, hosted at https://yourdomain.com/.well-known/assetlinks.json. It needs SHA-256 fingerprints of your app's signing certificate. The problem: Android has three different signing keys.

  1. Debug key: generated locally, works on your machine
  2. Upload key: used by your CI/CD pipeline
  3. Play Store key: the key Google re-signs your app with before distributing it

The Play Store key is the only one that matters for production users. Every developer who forgets this ships an app that works in testing and fails for 100% of Play Store users.

Get the correct fingerprint from Play Console under Setup > App signing > App signing key certificate > SHA-256 fingerprint.

2.6 Android: no wildcard subdomains#

iOS supports wildcards in Associated Domains (e.g. *.example.com). Android does not. Need login.example.com, api.example.com and www.example.com? That is three separate entries in assetlinks.json. Miss one and that subdomain cannot use passkeys. You can also configure credential sharing via Play Console, but the assetlinks.json file must still be valid.

Debugger Icon

Want to experiment with passkey flows? Try our Passkeys Debugger.

Try for Free

3. Platform APIs - two completely different implementations#

You survived domain association. Now you get to implement the same feature twice in two completely different ways.

3.1 iOS: ASAuthorizationController#

iOS passkey authentication requires this sequence:

  1. Create an ASAuthorizationPlatformPublicKeyCredentialProvider
  2. Build a registration or assertion request
  3. Create an ASAuthorizationController
  4. Set presentation context and delegate
  5. Call performRequests()

The order matters. Mess it up and you get crashes.

The critical encoding trap: WebAuthn uses Base64URL encoding. iOS returns raw Data objects. If you use a standard Base64 decoder instead of Base64URL, the + and / characters silently become - and _. Everything breaks. The server returns "Invalid Response" and nothing in the error tells you it is an encoding problem.

3.2 iOS: no UI control#

When you call performRequests(), iOS takes over the screen with a system sheet. You cannot customize it, theme it or change the text. If the user cancels, you get a generic ASAuthorizationError.canceled. If the presentation window is not visible (e.g. you triggered the request during a transition), it crashes.

Your app's design system is irrelevant here. Apple's system sheet is the UI.

3.3 iOS: iCloud Keychain sync lag#

User creates a passkey on their iPad. They pick up their iPhone. The passkey is not there yet.

iCloud Keychain sync is not instant. It depends on network conditions, iCloud account status and device state. Your code cannot check sync status, cannot force a sync and cannot detect whether a sync is in progress. The only option is graceful fallback. If the credential is not found, offer an alternative authentication method and let the user retry later.

3.4 Android: two APIs, one deprecated#

Android has two passkey APIs:

  1. FIDO2 API (com.google.android.gms.fido.fido2): the original implementation. Google now considers it legacy and recommends migrating to Credential Manager. It still works but is no longer receiving feature updates.
  2. Credential Manager (androidx.credentials): the current recommended API. Unifies passkeys, passwords and federated sign-in (Google Sign-In) into a single bottom sheet. Available via Jetpack, backported to Android 9+ through Play Services.

If you are starting fresh, use Credential Manager. If you have an existing FIDO2 API integration, Google provides a migration guide. The APIs are not compatible. Migrating is a rewrite, not a refactor.

3.5 Android: version and Play Services fragmentation#

Android passkey support depends on both the OS version and Google Play Services:

  • Android 14+: native Credential Manager support
  • Android 9-13: requires Google Play Services with Credential Manager backport
  • Outdated Play Services: random, hard-to-reproduce failures
  • No Play Services (Huawei): no passkey support at all

Check upfront:

val credentialManager = CredentialManager.create(context)

If credential creation or retrieval throws NoCredentialException on a device you expect to work, check the Play Services version first.

3.6 Android: OEM-specific behavior#

Pixel: works as documented (mostly).

Samsung: Samsung Pass and Google Password Manager can both register as credential providers. When both are active, users may see confusing duplicate prompts. If your app combines passkeys with Google Sign-In via GetGoogleIdOption (common in Credential Manager setups), multiple Google accounts on the device can trigger a TransactionTooLargeException that crashes the entire credential selection flow. This specific bug was documented on StackOverflow and fixed in Google Play Services 24.40+, but older versions are still widespread.

Xiaomi: some models report "Device doesn't support credentials" even though the hardware and OS version qualify. Xiaomi's ROM modifications interfere with Credential Manager.

Huawei (without Google Play Services): no support. You need a completely separate authentication path.

3.7 Android: screen lock prerequisite#

If the user has "Swipe to Unlock" (no PIN, pattern or biometric), passkeys silently fail. The error you get is a generic SecurityError with no indication that the problem is the lock screen configuration.

Check before attempting any passkey operation:

val keyguardManager = getSystemService(KeyguardManager::class.java) if (!keyguardManager.isDeviceSecure) { // tell the user to set up a PIN or biometric }

Note: isDeviceSecure (checks for PIN/pattern/password) is what you want, not isKeyguardSecure (which returns true even for swipe-to-unlock on some OEMs).

PasskeysCheatsheet Icon

Looking for a dev-focused passkey reference? Download our Passkeys Cheat Sheet. Trusted by dev teams at Ally, Stanford CS & more.

Get Cheat Sheet

4. WebView: where hybrid apps break#

If your app wraps a website in a WebView, you are in for additional pain.

4.1 Origin mismatch#

The origin in a passkey ceremony depends on the context:

  • Web browser: https://example.com
  • Android native: android:apk-key-hash:sha256_abc123...
  • iOS native: uses the RP ID directly

A passkey created in one context will not work in another unless your server explicitly accepts multiple origins. If you do not configure this, authentication fails with a "SecurityError" or, worse, a phishing detection warning.

Read our detailed blog post on origin validation in native apps for details.

4.2 WebView support timeline#

  • Before iOS 16 / Android 14: WebView passkey support does not exist
  • iOS 16+: WKWebView supports passkeys if the app has the correct Associated Domains entitlement
  • Android 14+: requires explicit opt-in via Jetpack Credential Manager integration

On Android, WebView passkey support is not enabled by default. You need the WebViewCredentialProvider from the Jetpack libraries and proper assetlinks.json configuration. The documentation is sparse.

4.3 JavaScript bridge: do not do this#

Some developers try to bridge native passkey APIs into WebView via JavaScript:

window.nativeBridge.authenticateWithPasskey()

This is a security risk. Any XSS vulnerability or malicious injected script can call your bridge and trigger authentication. Google's documentation explicitly warns against this pattern.

4.4 System Browser as fallback#

Using SFSafariViewController (iOS) or Chrome Custom Tabs (Android) for authentication works and avoids the origin mismatch problem. The trade-off: it looks like a browser, not your app. A URL bar is visible. Users get confused about whether they are still in your app. Returning to the app after authentication is not always seamless - deep link handling adds yet another layer of configuration.

5. Autofill integration#

You want passkeys to appear in the keyboard suggestions when the user taps a username field. This is Conditional UI and it is unreliable in native apps.

5.1 iOS autofill#

Set textContentType = .username on your text field. The passkey should appear in the QuickType bar. In practice:

  • If a third-party password manager (1Password, Dashlane) is installed and set as the autofill provider, it takes priority. The system passkey may not appear.
  • On first app launch, AASA verification may not have completed yet, so no passkeys show up.
  • There is no API to check whether autofill will show passkeys. You cannot know in advance whether the prompt will appear.

5.2 Android autofill#

Android's Credential Manager can auto-select a passkey if you set isAutoSelectAllowed = true. But the flow is fragile:

  • User taps away from the credential picker: GetCredentialCancellationException
  • User dismisses the bottom sheet: GetCredentialCancellationException
  • Multiple credential providers installed: the system shows a provider selection dialog before the actual credential picker

On Android 13 and below, the password save dialog and the passkey creation dialog can appear simultaneously after a login, fighting each other for the user's attention. Users click the wrong one and no passkey gets created.

Igor Gjorgjioski Testimonial

Igor Gjorgjioski

Head of Digital Channels & Platform Enablement, VicRoads

Corbado proved to be a trusted partner. Their hands-on, 24/7 support and on-site assistance enabled a seamless integration into VicRoads' complex systems, offering passkeys to 5 million users.

Passkeys that millions adopt, fast. Start with Corbado's Adoption Platform.

Start Free Trial

6. Platform lock-in#

6.1 Ecosystem boundaries#

A passkey created on an iPhone might live in iCloud Keychain. A passkey created on a Pixel might live in Google Password Manager. User switches platforms? The passkeys do not come along if the credential manager is not available on the other device.

This means your system still needs fallback authentication. If you did not build a recovery flow, the user who switches from iPhone to Android probably cannot log in.

The FIDO Alliance published working drafts of the Credential Exchange Protocol (CXP) and Credential Exchange Format (CXF) to address this. Apple, Google, Microsoft, 1Password, Dashlane and Bitwarden are all contributing. But there is no shipping implementation yet.

6.2 Cross-device authentication via QR codes#

The fallback for cross-platform use is hybrid transport - scan a QR code with your phone to authenticate on another device. It works, but:

  • Requires Bluetooth on both devices (for proximity verification)
  • Both devices need network connectivity
  • Android shows up to three separate Bluetooth permission dialogs

This is not a reliable primary flow. Treat it as a backup option.

7. Testing#

7.1 Simulators lie#

The iOS Simulator fakes Face ID / Touch ID with a simple toggle. The Android Emulator fakes fingerprint with adb commands. Neither accurately represents:

  • Actual Keychain / Credential Manager behavior
  • Bluetooth (does not exist in simulators)
  • iCloud sync
  • Play Services version quirks
  • OEM-specific ROM modifications

Bugs that crash production are invisible in simulators. See our native passkey testing guide for details.

7.2 Physical device matrix#

Minimum viable test set:

  • iPhone with Face ID (iPhone X or later)
  • iPhone with Touch ID (iPhone SE)
  • Pixel (Android reference device)
  • Samsung Galaxy (most popular Android OEM, tests Samsung Pass interaction)
  • At least one device running Android 9-12 (tests Play Services backport path)

Test scenarios that break in production:

  • No screen lock set (passkeys silently fail)
  • Dirty / wet fingerprint sensor (biometric rejection under real conditions)
  • Passkey revoked server-side while still cached on device
  • Multiple Google accounts signed in (Android)
  • Third-party password manager installed (iOS and Android)

7.3 Automation is limited#

UI testing frameworks (XCTest, Espresso) cannot interact with system biometric prompts. You cannot tap "Face ID" in an automated test. Options:

  • Use virtual authenticators in CI for the WebAuthn protocol layer
  • Manual QA for the biometric UI on physical devices
  • On Android, use BiometricManager.Authenticators.DEVICE_CREDENTIAL for testing without biometrics

There is no way around manual testing for the full end-to-end flow.

8. Conclusion#

Native passkey implementation is not a simple feature you can implement over night. It is more a sub-project. Passkeys are complex on every platform, but the native-specific layer - domain association, dual platform APIs, OEM fragmentation, WebView origin handling - adds tons of work on top of the challenges you already face on the web.

The domain association layer alone (AASA files, CDN caching, signing keys, assetlinks.json) can consume many resources. Then you implement the same feature twice in two incompatible APIs. Then you discover that every Android OEM behaves differently. Then you learn that your WebView approach needs a completely different origin configuration. Then you realize simulators hid half your bugs.

When it works, the UX is genuinely good. One tap, logged in, unphishable. But getting there requires budgeting the time honestly: plan for 3-6 months, not 2 weeks. Build fallback flows from day one. Test on physical devices. And read the WebAuthn errors guide before you ship.

Add passkeys to your app in <1 hour with our UI components, SDKs & guides.

Start Free Trial

Share this article


LinkedInTwitterFacebook