Implementing Passkeys in iOS with AuthenticationServices
A practical introduction to passwordless authentication using passkeys on iOS.
02 Mar 2026 · 7 min read
Passkeys are credentials built to eliminate security problems like weak or reused passwords, credential leaks and phishing attacks.
In this article, we'll look at what passkeys are, how they work conceptually, and how to integrate them into an iOS app using Apple's AuthenticationServices framework.

What are passkeys?
Passkeys are a passwordless authentication method based on public-key cryptography. Instead of storing a password on a server, passkeys use:
- A private key, securely stored on the user's device.
- A public key, stored on the server.
When signing in, the server sends a cryptographic challenge. The device signs it using the private key, and the server verifies the signature using the public key.
The private key is securely retained on the device and synced with other devices, for example via iCloud Keychain.
Under the hood, passkeys are built on the WebAuthn and FIDO2 standards. As iOS developers, however, we do not need to implement those protocols manually, the system handles that for us.
How passkeys work in an iOS app
On iOS, passkeys are implemented using the AuthenticationServices framework.
There are two main flows:
- Registration - creating a new passkey.
- Authentication - signing in using an existing passkey.
In both cases, the backend plays a crucial role. It must:
- Generate cryptographic challenges
- Store public keys
- Verify signed responses
The iOS app acts as a client in the WebAuthn flow.
Registering a passkey
A typical "sign up with passkey" flow looks like this:
- The user enters an identifier (for example an email) on the sign-up screen.
- The app asks the backend to start passkey registration.
- The backend returns a registration challenge and a stable user handle (a backend-generated user ID).
- iOS creates the passkey and returns attestation data.
- The app sends the attestation back to the backend, which verifies it and creates the account.
The same flow also works when a user is already signed in and wants to add a passkey as an additional login method.
After obtaining a challenge from the server, we can create an authentication request which presents a sheet to the user asking to create a new credential:
let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: "example.com")let registrationRequest = provider.createCredentialRegistrationRequest(challenge: challenge, displayName: email, userID: userID)let authController = ASAuthorizationController([platformKeyRequest])authController.delegate = selfauthController.presentationContextProvider = selfauthController.performRequests()
The relyingPartyIdentifier must match the associated domain configured via the Associated Domains capability.
When the user confirms with Face ID or Touch ID, the system creates a new key pair and returns a credential. In the delegate callback:
func authorizationController(controller: ASAuthorizationController,didCompleteWithAuthorization authorization: ASAuthorization) {if let credential = authorization.credentialas? ASAuthorizationPlatformPublicKeyCredentialRegistration {let credentialID = credential.credentialIDlet clientDataJSON = credential.rawClientDataJSONlet attestationObject = credential.rawAttestationObject// Send these values to backend for verification}}
The backend then verifies the registration and stores the public key.
Authenticating with a passkey
The login flow is similar. The only difference is, that we now create an assertion request instead of a registration request:
let request = credetialProvider.createCredentialAssertionRequestWithChallenge(challenge)
When providing an assertion request to the controller and the user has one or more credentials on the device, the device displays a sheet with the list of credentials to choose from. If there aren't any credentials on the device, we get an error. In this case, we can ask the user to register.
Based on the credential that is passed into authorizationController(controller:didCompleteWithAuthorization:), we can determine the request type and take further steps.
func authorizationController(controller: ASAuthorizationController,didCompleteWithAuthorization authorization: ASAuthorization) {switch authorization.credential {case let registration as ASAuthorizationPlatformPublicKeyCredentialRegistration:// Send attestation data to backend (finish registration)case let assertion as ASAuthorizationPlatformPublicKeyCredentialAssertion:let signature = assertion.signaturelet clientDataJSON = assertion.rawClientDataJSONlet authenticatorData = assertion.rawAuthenticatorDatalet credentialID = assertion.credentialID// Send signature and related data to backend (finish login)default:break}}
For authentication, the backend verifies the signature using the stored public key. If valid, the user is signed in.



Newsletter
Like to support my work?
Say hi
Related tags
Articles with related topics
Latest articles and tips



