> axiom-passkeys
Use when implementing passkey sign-in, replacing passwords with WebAuthn, configuring ASAuthorizationController, setting up AutoFill-assisted requests, adding automatic passkey upgrades, or migrating from password-based authentication. Covers passkey creation, assertion, cross-device sign-in, credential managers, and the Passwords app.
curl "https://skillshub.wtf/CharlesWiltgen/Axiom/axiom-passkeys?format=md"Passkeys
Passkey authentication for iOS apps — registration, assertion, AutoFill-assisted requests, automatic upgrades, combined credential flows, and migration from password-based auth.
When to Use This Skill
Use when you need to:
- ☑ Add passkey sign-in to an iOS app
- ☑ Replace password-based authentication with passkeys
- ☑ Configure ASAuthorizationController for passkey registration or assertion
- ☑ Set up AutoFill-assisted passkey requests (QuickType bar)
- ☑ Add automatic passkey upgrades for existing password users (iOS 18+)
- ☑ Support combined credential requests (passkey + password + Sign in with Apple)
- ☑ Configure associated domains for webauthn/webcredentials
- ☑ Debug passkey assertion failures or missing QuickType suggestions
Example Prompts
"How do I add passkey sign-in to my app?" "My passkeys aren't showing in the QuickType bar" "How do I migrate existing password users to passkeys?" "ASAuthorizationError.canceled — what's going wrong?" "How do I support both passkeys and passwords during migration?" "How do I set up associated domains for passkeys?" "What's the difference between performRequests and performAutoFillAssistedRequests?" "How do I add automatic passkey upgrades on iOS 18?"
Red Flags
Signs you're heading in the wrong direction:
- ❌ Still using passwords as primary auth when passkeys are available — Passkeys are not "extra security." They are the replacement. Every password-only sign-in is a phishing opportunity you're leaving open.
- ❌ Not annotating the username field with
.usernametextContentType — Without this, the system can't associate the field with passkey credentials. AutoFill won't suggest passkeys for unlabeled fields. - ❌ Using
performRequests()for the primary sign-in flow — this shows a modal sheet instead of putting passkeys in the QuickType bar. UseperformAutoFillAssistedRequests()for the primary path. ReserveperformRequests()for registration and explicit "Sign In" button taps. - ❌ Setting
userVerificationto"required"on the server — This prevents sign-in on devices without biometrics. The platform handles verification appropriately per device. Use"preferred"(the default). - ❌ Creating passkeys in an extension or non-main-app context — Passkey creation requires the main app target. Extensions can perform assertions but not registrations.
- ❌ Not supporting combined credential requests — During migration, users may have a passkey, a password, or neither. A single ASAuthorizationController handles all three. Offering only passkeys locks out users who haven't upgraded.
Why Passkeys
Passkeys are not an incremental improvement over passwords. They are a replacement architecture.
Phishing-proof: Each passkey is cryptographically bound to a specific domain. A fake login page on secure-myapp.com cannot trigger a passkey created for myapp.com. There is no credential to type into the wrong site.
No credential database to leak: The server stores only a public key. A breach exposes nothing usable — no password hashes to crack, no shared secrets to replay.
Single-tap sign-in: Face ID or Touch ID replaces typing. Registration and assertion are both one-tap flows.
FIDO Alliance standard: WebAuthn/CTAP2 protocol. Works across Apple, Google, and Microsoft platforms. Passkeys created on iPhone sync via iCloud Keychain and work on Mac, iPad, and the web.
Adoption: Apple ships passkeys as a first-class system feature. iCloud Keychain syncs them. The Passwords app manages them. Third-party credential managers (1Password, Dashlane) support them natively as of iOS 17.
Associated Domains Setup
Passkeys require an associated domain linking your app to your server. Without this, the system won't offer passkeys for your app.
1. Add the Entitlement
In Xcode: Target > Signing & Capabilities > + Associated Domains.
Add:
webcredentials:example.com
2. Host the AASA File
Serve /.well-known/apple-app-site-association from your domain over HTTPS with Content-Type: application/json:
{
"webcredentials": {
"apps": [
"TEAMID.com.example.myapp"
]
}
}
Requirements:
- HTTPS with a valid certificate (no self-signed)
- No redirects — the file must be served directly at the path
TEAMIDis your Apple Developer Team ID, not the bundle ID prefix- The file must be at the root domain, not a subdirectory
3. Verify
curl -s "https://example.com/.well-known/apple-app-site-association" | python3 -m json.tool
Apple's CDN caches the AASA file. Changes can take up to 24 hours to propagate. During development, enable Associated Domains Development in Developer Settings on the device and use the ?mode=developer query parameter.
Registration Flow
Registration creates a new passkey and stores it in the user's credential manager.
import AuthenticationServices
func registerPasskey(challenge: Data, userName: String, userID: Data) {
let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
relyingPartyIdentifier: "example.com"
)
let request = provider.createCredentialRegistrationRequest(
challenge: challenge,
name: userName,
userID: userID
)
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = self
controller.presentationContextProvider = self
controller.performRequests()
}
Key parameters:
relyingPartyIdentifier— Must match your associated domain (nohttps://prefix)challenge— Server-generated cryptographic challenge (use at least 16 random bytes). Never reuse challenges.name— Display name shown to the user in the passkey prompt and Passwords appuserID— Opaque identifier for the user account. Do not use email or username — use a random UUID or server-side user ID
Handling the Registration Response
func authorizationController(controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization) {
guard let credential = authorization.credential
as? ASAuthorizationPlatformPublicKeyCredentialRegistration else { return }
let attestationObject = credential.rawAttestationObject
let clientDataJSON = credential.rawClientDataJSON
let credentialID = credential.credentialID
// Send attestationObject, clientDataJSON, credentialID to your server
// Server validates and stores the public key
}
Registration uses performRequests() (modal) because the user explicitly chose to create a passkey. This is the one place where modal presentation is correct.
Assertion Flow (Sign-In)
Two paths for sign-in, each for a different UX context.
AutoFill-Assisted (Primary Path)
The preferred sign-in flow. Passkeys appear in the QuickType bar when the user taps a text field with .username content type. Single-tap sign-in with no modal interruption.
func signInWithAutoFill() {
let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
relyingPartyIdentifier: "example.com"
)
let request = provider.createCredentialAssertionRequest(
challenge: serverChallenge
)
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = self
controller.presentationContextProvider = self
controller.performAutoFillAssistedRequests()
}
Critical details:
- Call
performAutoFillAssistedRequests()early — before the user focuses the username field. Call it inviewDidAppearor when the sign-in view appears. - The username
UITextFieldmust have.textContentType = .usernameset. Without this, the QuickType bar won't show passkey suggestions. - Do not set
allowedCredentialson the request. AutoFill needs to show all available passkeys for the domain. - The request stays active until the user selects a credential, navigates away, or you cancel it.
Modal (Fallback Path)
Use when the user taps a "Sign In" button explicitly, or when you know the username and want to request a specific credential.
func signInWithModal(allowedCredentials: [ASAuthorizationPlatformPublicKeyCredentialDescriptor]? = nil) {
let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
relyingPartyIdentifier: "example.com"
)
let request = provider.createCredentialAssertionRequest(
challenge: serverChallenge
)
if let allowedCredentials {
request.allowedCredentials = allowedCredentials
}
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = self
controller.presentationContextProvider = self
controller.performRequests()
}
Use allowedCredentials when you know the user's credential IDs (e.g., the user typed their username and your server returned their registered credential IDs). This narrows the passkey selection to that account.
Handling the Assertion Response
func authorizationController(controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization) {
guard let credential = authorization.credential
as? ASAuthorizationPlatformPublicKeyCredentialAssertion else { return }
let signature = credential.signature
let clientDataJSON = credential.rawClientDataJSON
let authenticatorData = credential.rawAuthenticatorData
let credentialID = credential.credentialID
let userID = credential.userID
// Send to server for verification
}
Automatic Passkey Upgrades (iOS 18+)
Silently upgrade password users to passkeys without interrupting their flow. The system shows a brief notification confirming the upgrade — no modal, no extra taps.
How It Works
When a user signs in with a password, the system can automatically create a passkey for the same account. This happens when:
- The credential manager supports automatic upgrades
- The user just successfully authenticated with a password for the same account
- Your app requests a conditional registration
Implementation
func requestAutomaticUpgrade(challenge: Data, userName: String, userID: Data) {
let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
relyingPartyIdentifier: "example.com"
)
let request = provider.createCredentialRegistrationRequest(
challenge: challenge,
name: userName,
userID: userID
)
request.requestStyle = .conditional
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = self
controller.presentationContextProvider = self
controller.performAutoFillAssistedRequests()
}
Key detail: .requestStyle = .conditional makes the registration opportunistic. It will succeed silently when conditions are right and fail silently when they're not. Do not treat the failure callback as an error — it means conditions weren't met this time.
When to call: After the user successfully authenticates with a password. Check first whether the user already has a passkey for this account — don't request an upgrade if they do.
Server Requirements
Your server must be prepared for an asynchronous registration that arrives shortly after a password sign-in. The userID and challenge must be valid and associated with the session.
Combined Credential Requests
During migration, your users may have passkeys, passwords, or Sign in with Apple credentials. A single ASAuthorizationController handles all three.
func signInWithCombinedRequest() {
var requests: [ASAuthorizationRequest] = []
// Passkey assertion
let passkeyProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(
relyingPartyIdentifier: "example.com"
)
requests.append(
passkeyProvider.createCredentialAssertionRequest(challenge: serverChallenge)
)
// Password
let passwordProvider = ASAuthorizationPasswordProvider()
requests.append(passwordProvider.createRequest())
// Sign in with Apple
let appleIDProvider = ASAuthorizationAppleIDProvider()
requests.append(appleIDProvider.createRequest())
let controller = ASAuthorizationController(authorizationRequests: requests)
controller.delegate = self
controller.presentationContextProvider = self
controller.performAutoFillAssistedRequests()
}
Handling Multiple Credential Types
func authorizationController(controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization) {
switch authorization.credential {
case let credential as ASAuthorizationPlatformPublicKeyCredentialAssertion:
// Passkey sign-in — verify with server
handlePasskeyAssertion(credential)
case let credential as ASPasswordCredential:
// Password sign-in — verify, then offer passkey upgrade
handlePasswordSignIn(credential)
case let credential as ASAuthorizationAppleIDCredential:
// Apple ID sign-in
handleAppleIDSignIn(credential)
default:
break
}
}
After a successful password sign-in, call the automatic upgrade flow to progressively migrate users to passkeys.
Cross-Device Sign-In
Users can sign in on a device that doesn't have their passkey by using their phone as an authenticator.
How it works:
- Your app presents a passkey assertion request
- The system shows a QR code on the device requesting sign-in
- The user scans the QR code with their phone (which has the passkey)
- Bluetooth proximity verification confirms the phone is physically nearby
- The user authenticates with Face ID/Touch ID on their phone
- The assertion completes on the original device
No app changes required. This is a system-level feature. Any device that supports passkeys can act as a cross-device authenticator. The communication is end-to-end encrypted through an Apple relay server.
Bluetooth required: Both devices must have Bluetooth enabled. This is the proximity check that prevents remote phishing — the authenticating device must be physically near the requesting device.
Migration Strategy
Phase 1 — Add Passkey Support Alongside Passwords
Keep existing password auth. Add passkey registration and assertion. Use combined credential requests so both paths work.
Server changes: Add WebAuthn endpoints for registration and assertion. Store public keys alongside password hashes. Both auth methods validate to the same user session.
App changes: Implement registration flow (offer after password sign-in), assertion flow (AutoFill-assisted), and combined requests.
Phase 2 — Automatic Upgrades (iOS 18+)
Add conditional registration requests after password sign-ins. Users silently migrate to passkeys over time. Track upgrade metrics to measure adoption.
No user action required. The system handles the upgrade transparently.
Phase 3 — Reduce Phishable Factors
For accounts with passkeys, consider:
- Removing password reset flows (passkeys don't need them)
- Dropping SMS 2FA (passkeys are inherently two-factor: device possession + biometric)
- Offering account recovery via passkey on another device instead of email/SMS
Do not force-remove passwords. Let users choose to go passwordless. Some users need password access from devices that don't support passkeys.
Passwords App Integration (iOS 18+)
The Passwords app displays your app's name and icon using OpenGraph metadata from your associated domain. Add to your website's <head>:
<meta property="og:title" content="MyApp" />
<meta property="og:image" content="https://example.com/icon.png" />
This is how your app appears in the user's credential manager. Without it, the Passwords app shows only the domain name.
Anti-Rationalization
| Rationalization | Reality | Time Cost |
|---|---|---|
| "Passwords are fine for now" | Every password sign-in is a phishing vector. Credential stuffing attacks cost real money — the average breach costs $4.5M. Passkeys eliminate the entire attack surface. | Ongoing risk vs 2-3 days to implement |
| "We'll add passkeys later" | AutoFill-assisted passkey requests are the same amount of integration work as a custom password text field with AutoFill. You're not saving time by deferring. | Same implementation effort either way |
| "Users won't understand passkeys" | Users don't need to understand public-key cryptography. They see "Sign in with Face ID" — one tap. Apple, Google, and Microsoft are shipping passkeys as the default across all platforms. | 0 extra user education needed |
| "Our server doesn't support WebAuthn" | Server-side WebAuthn libraries exist for every major backend (Python, Node, Go, Ruby, Java, .NET). Most are well-tested and actively maintained. | 1-2 days server-side integration |
| "What about users without biometrics?" | Device passcode is a valid user verification method. Every supported device has at least a passcode. Setting userVerification to "preferred" lets the platform handle this correctly. | 0 extra work — platform handles it |
| "We need password as fallback forever" | Combined credential requests support passwords and passkeys simultaneously. Use automatic upgrades to progressively migrate. You can keep passwords indefinitely while passkeys become primary. | No forced choice — run both |
Pressure Scenarios
Scenario 1: "Our users aren't ready for passkeys"
Context: Product manager pushes back on passkey adoption, citing user confusion risk.
Pressure: "Our users are not technical. They won't understand what a passkey is. Let's stick with passwords and add passkeys next year."
Reality: Apple ships passkeys as a built-in system feature across every platform — iPhone, iPad, Mac, Apple Watch, Windows via cross-device auth. Users see "Sign in with Face ID" in the QuickType bar. They do not see "WebAuthn CTAP2 public-key credential." The Passwords app manages passkeys alongside passwords transparently. Apple's own account system, Google accounts, and Microsoft accounts all use passkeys. Your users are already using them elsewhere.
Correct action: Implement combined credential requests. Existing password users keep signing in with passwords. Passkeys appear automatically for users whose credential managers support them. Add automatic upgrades (iOS 18+) to progressively migrate without user action.
Push-back template: "Users don't need to understand passkeys. They see 'Sign in with Face ID' — one tap, done. Apple, Google, and Amazon already use passkeys for their own sign-in. We add it alongside passwords, so nobody's flow changes. Users who get passkeys automatically get a better experience; everyone else continues as before."
Scenario 2: "Just ship password auth now, add passkeys later"
Context: Deadline pressure on a new app. Developer wants to defer passkey support to a post-launch update.
Pressure: "We need to ship by Friday. Password auth works. We'll add passkeys in the next sprint."
Reality: Implementing AutoFill-assisted passkey requests is comparable in effort to building a polished password text field with AutoFill support, secure storage, and "forgot password" flows. You're building the ASAuthorizationController integration either way — the question is whether you wire up one provider (passwords) or three (passkeys + passwords + Apple ID). Combined requests add ~30 lines to the delegate.
Correct action: Implement combined credential requests from the start. The server needs WebAuthn endpoints, but client-side the work is nearly identical. Shipping with passkey support from day one means you never have to retrofit it, and you avoid the "next sprint" that turns into "next quarter."
Push-back template: "AutoFill-assisted passkeys use the same ASAuthorizationController we'd use for password AutoFill. Adding passkey support is ~30 lines in the delegate — not a sprint of work. Shipping without it means we build the password flow now and rebuild the auth flow later to add passkeys. Let's do it once."
Checklist
Before shipping passkey authentication:
Associated Domains:
-
webcredentials:yourdomain.comadded to Associated Domains capability - AASA file served at
/.well-known/apple-app-site-associationover HTTPS - AASA file contains correct Team ID and bundle identifier
- No redirects on the AASA file path
- AASA changes propagated (or developer mode enabled for testing)
Registration:
- Server generates unique challenge per registration attempt
-
userIDis opaque (not email or username) -
relyingPartyIdentifiermatches associated domain exactly - Registration uses
performRequests()(modal — correct for explicit creation) - Server stores credentialID and public key after successful registration
Assertion (Sign-In):
- Username text field has
.textContentType = .username - AutoFill-assisted request called early (before field focus)
- AutoFill path uses
performAutoFillAssistedRequests() - Modal fallback available via
performRequests()for explicit sign-in button -
allowedCredentialsnot set on AutoFill-assisted requests - Server validates signature, authenticatorData, and clientDataJSON
Combined Requests (if supporting multiple auth methods):
- Passkey, password, and Apple ID providers all included
- Delegate handles all credential types in switch statement
- Password sign-in triggers automatic passkey upgrade flow
Automatic Upgrades (iOS 18+):
- Registration request uses
.requestStyle = .conditional - Upgrade request uses
performAutoFillAssistedRequests() - Failure callback treated as "conditions not met" — not an error
- Existing passkey checked before requesting upgrade
Error Handling:
-
ASAuthorizationError.canceledhandled gracefully (user dismissed — not an error) -
ASAuthorizationError.failedlogged with context for debugging - Network failures during server verification don't leave auth in inconsistent state
Resources
WWDC: 2022-10092, 2024-10125
Docs: /authenticationservices, /authenticationservices/public-private-key-authentication/supporting-passkeys
Skills: axiom-keychain
> related_skills --same-repo
> axiom-keychain
Use when storing credentials, tokens, or secrets securely, debugging SecItem errors (errSecDuplicateItem, errSecItemNotFound, errSecInteractionNotAllowed), managing keychain access groups, or choosing accessibility classes. Covers SecItem API mental model, uniqueness constraints, data protection, biometric access control, sharing between apps, and Mac keychain differences.
> axiom-keychain-ref
Use when needing SecItem function signatures, keychain attribute constants, item class uniqueness constraints, accessibility level details, SecAccessControlCreateFlags, kSecReturn behavior per class, LAContext keychain integration, or OSStatus error codes. Covers complete keychain API surface.
> axiom-keychain-diag
Use when SecItem calls fail — errSecDuplicateItem from unexpected uniqueness, errSecItemNotFound despite item existing, errSecInteractionNotAllowed in background, keychain items disappearing after app update, access group entitlement errors, or Mac keychain shim issues. Covers systematic error diagnosis with decision trees.
> speech
Use when implementing speech-to-text, live transcription, or audio transcription. Covers SpeechAnalyzer (iOS 26+), SpeechTranscriber, volatile/finalized results, AssetInventory model management, audio format handling.