Passkey — Passwordless solution for authentication
Passkey is a passwordless solution from FIDO alliance to resolve the global password problem. It provides a simple user experience with phishing-resistant security and can be adopted in mobile and web applications. In this article, we are going to investigate this solution and discuss a how to implement it on iOS platform.
If you are not familiar with passkey technology yet and want to experience it before diving in, please go to https://webauthn.io/ or https://passkeys-demo.appspot.com/ to try it out.
Follow me if you are interested in topics like this.
Benefits of using passkeys
Before we dive into the implementation, let’s check how we can benefit from adopting this solution. Compared with the other common ways to authenticate users in this digital world, passkeys stand out as shown in the slide below(Sourced from one WWDC session here).
How passkeys work
Passkeys rely on standard public key cryptography techniques. In essence, passkeys are a pair of private key and public key, where the public key is stored on the server and associated with a user account and the private key is stored locally for future authentications.
During the authentication process, the client app generates a signature using private key and the server verifies its authenticity using the public key stored on server like below:
(Sourced from one WWDC session here)
The platforms that support passkeys rely on local authentication methods such as biometrics, local PIN etc. to work with the above passkeys flow. Users can only register their passkeys or select passkeys to authenticate when they pass the local authentication. If biometrics authentication method is used, the whole process does not involve any passwords, which provides a smooth and effective authentication flow. To be noted, some websites like GitHub have already brought this into practice where we can really benefit from this innovative technology.
Server side logic
Passkey solution requires certain logics to be implemented on both client and server applications. The minimum requirement on the server side are the following:
When registering a passkey, the server needs to provide parameters like challenge etc. After client app provides the public key of the passkeys and signature of the challenge data, the server verifies the created public key credential sent from the client and stores the public key in the database.
At the beginning of the authenticating process, the server issue a challenge again. Then the client app sign it with its private key, the server validates verify the signature to decide if let the user sign in.
I have created a stub server for reference purpose, which can be found here. The registration related APIs are done at endpoints /register/start and /register/finish and the authentication related APIs are done at endpoints /authenticate/start and /authenticate/finish in the sample code.
There are some open source libraries that can be used if don’t want to implement all these from scratch. Please check out the following links:
https://passkeys.dev/docs/tools-libraries/libraries/
https://github.com/herrjemand/awesome-webauthn/blob/main/README.md
https://fidoalliance.org/certification/fido-certified-products/
Implement passkey for iOS applications
Since iOS 15, Apple provides support for passkeys. The main steps of implementing passkey on iOS applications are:
1. Setup Associated Domain
The url here should be perfixed with webcredentials and the server side should have the .well-known/apple-app-site-association file like below:
{
"webcredentials": {
"apps": [
"HBBLSCM2ZS.com.timwang.PasskeyAppiOSDemo"
]
}
}
Please note, if using a server that cannot be accessed from internet, we need to add ?mode=developer suffix to indicate the server is private.
2. Register passkeys
The registration passkeys flow are like below. Firstly get userID and challenge data from server(for example /register/start api), the create registrationRequest using method createCredentialRegistrationRequest of ASAuthorizationPlatformPublicKeyCredentialProvider. Lastly, create an instance of ASAuthorizationController and setup delegate and presentationContextProvider and call performRequests method to start registration flow.
func register() {
let publicKeyCredentialProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: domain)
// Call register/start api to get challenge & user ID
let challenge: Data = ... // returned from /register/start api
let userID: String = ... // returned from /register/start api
let registrationRequest = publicKeyCredentialProvider.createCredentialRegistrationRequest(challenge: challenge,
userID: userID)
let authController = ASAuthorizationController(authorizationRequests: \[registrationRequest\])
authController.delegate = self
authController.presentationContextProvider = self
authController.performRequests()
}
The UI flow are like below. In order to finish this flow properly, we need to handle delegate events, which will be covered in Handle events in delegate section.
3. SignIn using passkeys
To sign in using passkeys, similarly, we need to create an instance of ASAuthorizationController , but this time, we need assertionRequest , which is created using createCredentialAssertionRequest method of ASAuthorizationPlatformPublicKeyCredentialProvider.
func signIn() {
let publicKeyCredentialProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: domain)
let challenge = Challenge(user: user).challenge // Note: Demo only. Should call authentication/start api to get this challenge
let assertionRequest = publicKeyCredentialProvider.createCredentialAssertionRequest(challenge: challenge)
let authController = ASAuthorizationController(authorizationRequests: \[assertionRequest\])
authController.delegate = self
authController.presentationContextProvider = self
authController.performRequests()
}
The UI flow of signing in are like below. Similarly, we also need to handle delegate events.
4. Popup autofill sheet and sign in
To improve user experience, iOS platform provides an autofill sheet to allow users to select passkeys to sign in. This flow is usually starts after user inputs user name.
func autoFillAssistedPasskeySignIn() {
let publicKeyCredentialProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: domain)
let challenge = Challenge(user: user).challenge // Note: Demo only. Should call authentication/start api to get this challenge
let assertionRequest = publicKeyCredentialProvider.createCredentialAssertionRequest(challenge: challenge)
let authController = ASAuthorizationController(authorizationRequests: \[assertionRequest\])
authController.delegate = self
authController.presentationContextProvider = self
authController.performAutoFillAssistedRequests()
}
The UI flow of selecting passkeys are shown below:
5. Handle events in delegate
All the above flows that using ASAuthorizationController to perform requests, we need to handle the events in the delegate like below:
func authorizationController(controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization) {
if let credential = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialRegistration {
// Call register/finish api to verify the attestationObject and clientDataJSON of credential
// save attestationObject(the public key) to server and sign in the user with the new account if passed verification
} else if let credential = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialAssertion {
// Call authentication/finish api to verify the signature and clientDataJSON on the server for the given userID.
// Sign in the user with the new account if passed verification
} else {
print("Other logging in cases such as Sign in with Apple etc.")
}
}
A sample implementation can be found in this repo.
Other considerations
We have explored some basic aspects of adopting passkeys in a system. In order to fully adopt this solution, we might need to consider more scenarios:
- Allow existing users to add passkeys so that they can leverage this new solution in the future.
- Allow user to add more than one passkeys, which adds flexibilities for users to use multiple devices to log in the system and also make them easier to upgrade their devices.
- Allow user to delete some passkeys, which can be very useful when they lost their devices. Even though it might not be a risk as new holder of the devices cannot pass the local authorisation to use the passkeys. However, allowing user to delete the passkeys that are not used any more, will give users better experiences.
- Be ware of the fact that passkeys can be shared across devices. This can be done in various ways: Firstly, the iOS system relies on iCloud Keychain to sync passkeys onto different devices that logged in with same apple ID account. Also, user actively share passkey to others in settings app.
- Passkeys solution supports
Signing in using Nearby Devices, which, in my opinion, is a very effective way to sign users in web applications using their mobile devices.
Sharing is caring!