How does Passkeys behave across iOS 15 → iOS 16 when using ASAuthorizationController?

Summary

This postmortem analyzes how Passkeys behave when an app uses ASAuthorizationController across iOS 15 → iOS 16, why the behavior differs, and what engineering patterns prevent subtle authentication regressions. The core issue: iOS 15 does not support Passkeys, so the system silently falls back to traditional WebAuthn platform credentials that are not Passkeys and do not migrate when the device upgrades to iOS 16.

Root Cause

The underlying cause is that Passkeys were introduced in iOS 16, while iOS 15 only supports WebAuthn platform credentials without iCloud Keychain sync. As a result:

  • ASAuthorizationPlatformPublicKeyCredentialProvider exists on iOS 15, but Passkeys do not.
  • iOS 15 creates device‑bound WebAuthn credentials, not Passkeys.
  • These credentials cannot be upgraded to Passkeys because they lack the required iCloud Keychain sync metadata.
  • After upgrading to iOS 16, the system does not retroactively convert old credentials.

Why This Happens in Real Systems

Real authentication stacks behave this way because:

  • APIs evolve before features: Apple shipped the API in iOS 15 but the full Passkey ecosystem only in iOS 16.
  • Credential formats differ:
    • iOS 15 → local-only WebAuthn credential
    • iOS 16 → synchronized Passkey credential
  • Security constraints prevent auto‑migration:
    • Migration would require re‑attestation and user verification.
    • The original credential lacks the cryptographic properties required for Passkeys.

Real-World Impact

Teams often encounter:

  • Unexpected fallback behavior on iOS 15.
  • Users upgrading to iOS 16 but not seeing Passkeys until they re-authenticate.
  • Confusing UX if the app assumes Passkeys exist after OS upgrade.
  • Inconsistent credential storage across versions.

Example or Code (if necessary and relevant)

Below is a minimal example showing how senior engineers gate Passkey logic:

if #available(iOS 16.0, *) {
    let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: rpId)
    let request = provider.createCredentialRegistrationRequest(challenge: challenge)
    authController.authorizationRequests = [request]
} else {
    // iOS 15 fallback: use Sign in with Apple or password-based WebAuthn
    let appleIDProvider = ASAuthorizationAppleIDProvider()
    let request = appleIDProvider.createRequest()
    request.requestedScopes = [.fullName, .email]
    authController.authorizationRequests = [request]
}

How Senior Engineers Fix It

Experienced engineers avoid pitfalls by:

  • Explicitly gating Passkey flows behind iOS 16+ checks.
  • Treating iOS 15 WebAuthn credentials as non‑migratable.
  • Requiring a fresh authentication on iOS 16 to enroll a Passkey.
  • Designing UX that gracefully transitions users after OS upgrades.
  • Logging OS-version‑specific authentication paths to detect regressions.

Why Juniors Miss It

Less experienced engineers often overlook:

  • API availability ≠ feature availability.
  • Credential type differences between iOS versions.
  • Silent fallback behavior that hides the absence of Passkeys.
  • The need for explicit OS gating, assuming the system “just handles it.”
  • The fact that upgrading iOS does not upgrade credentials.

They assume the API behaves uniformly across versions, but authentication systems rarely work that way.

Leave a Comment