---
url: 'https://www.corbado.com/blog/native-ios-android-passkey-implementation-challenges'
title: 'Native Passkey Challenges: iOS & Android Pitfalls'
description: 'Native passkey implementation is 100x harder than web. Discover every iOS AASA CDN trap, Android signing key mistake and WebView pitfall before you ship.'
lang: 'en'
author: 'Vincent Delitz'
date: '2026-02-19T15:24:22.143Z'
lastModified: '2026-03-25T10:01:36.113Z'
keywords: 'native passkey implementation challenges, iOS passkey pitfalls, Android passkey signing key, AASA CDN cache, assetlinks passkey, passkey webview, credential manager vs fido2 api'
category: 'Passkeys Implementation'
---

# Native Passkey Challenges: iOS & Android Pitfalls

## Key Facts

- **Native passkey** implementation realistically takes 3-6 months, not 2 weeks: domain
  association, dual platform APIs, OEM fragmentation and WebView origin handling all add
  significant scope.
- **Apple's AASA CDN** caches your domain association file regardless of your origin
  headers, with no programmatic purge option, causing unpredictable propagation delays
  after updates.
- Android has three signing keys. Only the **Play Store key** matters for production
  users. Using the wrong one silently breaks passkeys for every Play Store user.
- About 5% of users encounter 'Application is not associated with this domain' immediately
  after install, as iOS may not have verified the AASA file yet.

## 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](https://www.corbado.com/blog/webauthn-errors) demo somewhere: tap
[Face ID](https://www.corbado.com/faq/is-face-id-passkey), user is logged in. The pitch writes itself: no
passwords involved, better [conversion rates](https://www.corbado.com/blog/logins-impact-checkout-conversion),
[phishing](https://www.corbado.com/glossary/phishing)-resistant MFA. A dream for frictionless onboarding and
login in [iOS](https://www.corbado.com/blog/webauthn-errors) and [Android](https://www.corbado.com/blog/how-to-enable-passkeys-android)
apps.

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

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](https://www.corbado.com/blog/passkeys-vs-password-managers)
  interference and
- driving [passkey adoption](https://www.corbado.com/blog/passkey-adoption-business-case).

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](https://www.corbado.com/blog/how-to-enable-passkeys-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](https://www.corbado.com/blog/how-to-enable-passkeys-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`:

```json
{
    "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](https://www.corbado.com/blog/webauthn-errors) 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](https://stackoverflow.com/questions/68368233/apple-app-site-association-file-is-not-fetched-from-server-but-cached-at-apple)
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](https://developer.apple.com/documentation/xcode/supporting-associated-domains)
  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](https://www.corbado.com/blog/ios-18-passkeys-automatic-passkey-upgrades)+ 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](https://www.corbado.com/blog/how-to-enable-passkeys-android) 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.

## 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](https://www.corbado.com/glossary/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](https://www.corbado.com/faq/private-key-sync-passkeys) 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](https://developers.google.com/identity/fido/android/native-apps)
   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](https://www.corbado.com/glossary/fido2) API integration, Google provides a
[migration guide](https://developers.google.com/identity/android-credential-manager). 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:

```kotlin
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](https://www.corbado.com/blog/samsung-passkeys) Pass and
[Google Password Manager](https://www.corbado.com/blog/how-to-use-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](https://stackoverflow.com/questions/78538579/new-google-credential-manager-is-throwing-a-transactiontoolargeexception)
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:

```kotlin
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).

## 4. WebView: where hybrid apps break

If your app wraps a website in a [WebView](https://www.corbado.com/blog/native-app-passkeys), 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](https://www.corbado.com/blog/webauthn-errors)" or, worse, a [phishing](https://www.corbado.com/glossary/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](https://www.corbado.com/blog/native-app-passkeys) passkey support
  does not exist
- **iOS 16+**: [WKWebView](https://www.corbado.com/blog/native-app-passkeys) 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](https://www.corbado.com/blog/native-app-passkeys) 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:

```javascript
window.nativeBridge.authenticateWithPasskey();
```

This is a security risk. Any XSS [vulnerability](https://www.corbado.com/glossary/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](https://www.corbado.com/glossary/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](https://www.corbado.com/blog/passkeys-vs-password-managers) (1Password,
  [Dashlane](https://www.corbado.com/blog/dashlane-passkeys)) 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](https://www.corbado.com/blog/how-to-enable-passkeys-android) 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](https://www.corbado.com/blog/passkey-creation-best-practices) 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.

## 6. Platform lock-in

### 6.1 Ecosystem boundaries

A passkey created on an iPhone might live in [iCloud Keychain](https://www.corbado.com/glossary/icloud-keychain).
A passkey created on a Pixel might live in
[Google Password Manager](https://www.corbado.com/blog/how-to-use-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](https://www.corbado.com/glossary/fido-alliance) published working drafts of the Credential
Exchange Protocol (CXP) and
[Credential Exchange Format](https://www.corbado.com/blog/credential-exchange-protocol-cxp-credential-exchange-format-cxf)
(CXF) to address this. Apple, Google, Microsoft,
[1Password](https://www.corbado.com/blog/1password-passkeys-best-practices-analysis),
[Dashlane](https://www.corbado.com/blog/dashlane-passkeys) and
[Bitwarden](https://www.corbado.com/blog/passkey-analysis-bitwarden-developer-survey-2024) 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](https://www.corbado.com/blog/qr-code-login-authentication) 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](https://www.corbado.com/faq/is-face-id-passkey) / 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](https://www.corbado.com/blog/testing-passkeys) guide for details.

### 7.2 Physical device matrix

Minimum viable test set:

- iPhone with [Face ID](https://www.corbado.com/faq/is-face-id-passkey) (iPhone X or later)
- iPhone with Touch ID (iPhone SE)
- Pixel (Android reference device)
- [Samsung](https://www.corbado.com/blog/samsung-passkeys) Galaxy (most popular Android OEM, tests
  [Samsung](https://www.corbado.com/blog/samsung-passkeys) 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](https://www.corbado.com/blog/passkeys-vs-password-managers) 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](https://www.corbado.com/glossary/authenticator) 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. For a comprehensive
guide covering functional, automated and non-functional
[passkey testing](https://www.corbado.com/blog/testing-passkeys) strategies, see our enterprise testing article.

## 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](https://www.corbado.com/blog/webauthn-errors) guide before you ship.

## Frequently Asked Questions

### How do I test iOS passkey changes without waiting for Apple's AASA CDN to update?

Add `?mode=developer` to your Associated Domains entitlement, enable Developer Mode on
your test device and turn on 'Associated Domains Development' in Settings. The device then
fetches the AASA file directly from your server, bypassing the CDN. Remove the
`?mode=developer` flag before App Store submission or your app will be rejected.

### Why do passkeys fail in my hybrid or WebView-based mobile app with a SecurityError?

Passkeys created in a browser use the origin `https://example.com`, while Android native
apps use an `android:apk-key-hash:` origin. If your server does not explicitly accept
multiple origins, authentication fails with a SecurityError or triggers a phishing
detection warning. WebView passkey support also requires iOS 16+ or Android 14+, and on
Android it is not enabled by default, requiring the WebViewCredentialProvider from Jetpack
libraries.

### Why do passkeys silently fail on some Android devices even when the OS version qualifies?

If the user has no PIN, pattern or biometric lock set, passkeys fail silently with a
generic SecurityError that gives no indication the screen lock is the cause. Check
`KeyguardManager.isDeviceSecure()` before any passkey operation and prompt the user to
configure a screen lock. Huawei devices without Google Play Services have no passkey
support at all and require a completely separate authentication path.

### Can a passkey created on an iPhone be used on an Android device?

Not automatically. A passkey stored in iCloud Keychain stays within the Apple ecosystem
and a passkey in Google Password Manager stays within Android. The FIDO Alliance has
published working drafts of the Credential Exchange Protocol (CXP) with Apple, Google,
Microsoft, 1Password, Dashlane and Bitwarden all contributing, but there is no shipping
implementation yet.

### What causes TransactionTooLargeException in Android Credential Manager and how do I fix it?

This crash occurs when multiple Google accounts are signed in on a device and your app
combines `GetGoogleIdOption` with passkeys in a single Credential Manager request. The bug
was fixed in Google Play Services 24.40+, but older versions remain widespread. As a
mitigation, check the Play Services version before making the request and consider
separating the Google Sign-In flow from the passkey credential selection flow.
