Passkey on Android: the little details that lead to big problems
Introduction
Passkey is a technology created and maintained by the FIDO alliance, created by numerous well-known companies like Google, Microsoft or Apple. The goal of this technology, which is an implementation of the FIDO2 standards, is to replace and suppress the need for passwords.
The technology is, at the time of the writing, in constant evolution, which makes it more difficult to develop and maintain.
A few weeks ago, I decided to develop an Android application that would this technology as its authentication mechanism to understand how it works. This was not as easy as I though because many requirements for the Android implementation to work are not regrouped in one place and are scattered across the web.
The goal of this post is to solve this issue and give a few highlights of the common traps you may fall into when developing an Android application that uses passkeys.
Most of the requirements are to be met server-wise, nothing to do with the code of your application per say. We will start by those so you can start testing your passkeys communications in good conditions.
A Well-known file
For the Android’s passkey implementation to accept to communicate with your server, Google, which has the lead on Android, needs to be able to check the “/.well-known/assetlinks.json” on your server. This file must contain some information about the Android applications that have the permission to communicate with the server. Google provided a tool to help developers to test their file.
All the required information and constraints can be found here. From this documentation, we can learn that the following constraints must hold for the file (directly quoted):
- It must be publicly available, and not behind a VPN.
- It must be served with Content-type: application/json.
- It must be accessible over HTTPS.
- It must be served directly with an HTTP 200 response (no HTTP 300’s redirect).
- Ensure no robots TXT prevents it:
- User-agent: *
- Allow: /.well-known/
- User-agent: *
A registered domain
Did you noticed that I said that “Google needs to check” and not “Android needs to check”, it’s because Android will ask to a Google server if your server is “legitimate” and not check it itself. So, if your server is not accessible from the internet, the Android’s passkey API will refuse to communicate and will give you some cryptic error:
Moreover, as far as I know, simply using an IP address will not work, your server must be behind a domain name to be checked by the Google server.
An Origin that is not one
During the authentication process, the request containing the response to the challenge, a random string that has been signed using the private key of the selected passkey entry, also contains an “Origin” field in the clientData parameter. So far, there is no issue… until you read the content of the Origin, which is a signature of the certificate used to sign the APK, not an Origin in the traditional HTTP meaning. For example, if we take the clientDataJson from the credential-manager, we can observe this:
Figure 1. Decoding the clientDataJSON with CyberChef
This can be a really bad news if, like me, you used a library like passport-fido2-webauthn to help you handle passkeys on the server-side. This is an issue because the library does not support these Origins, that have been generated by the Android’s passkey API, inside the clientdata of the requests. Meaning that your server will refuse all authentication requests from your Android application.
To circumvent this issue, you will need to find a way to register the certificate signature in your configurations so that the server accepts it. This can mean that you will need to patch the library yourself if it does not support Android’s Origins. For those of you who decided to use the same library, I made a PR to solve the issue.
The last part of the puzzle
This is the only part of this post that is about the code of the Android application itself. To use passkey with the Android’s API there are two main functions that need to be used with specific arguments:
- createCredentialAsync(): Create a passkey key pair and sign a challenge
- getCredentialAsync(): Use an already existing passkey key pair to sign a challenge
To use these, a CredentialManager object is required, it can be obtained by creating one using the application context: CredentialManager.create(getApplicationContext())
This is pretty simple if we ignore one of their parameters: the request.
For creating a new passkey key pair, a JSON respecting a specific format has to be build. The required fields and format of the JSON can be found here.
For reference, here is an example of code I made a few days ago:
Figure 2. Generating a Passkey key pair
Figure 3. The function used to build the options
Conclusion
Passkey is a great technology which evolves quickly. This comes at a price: not all the prerequisites to make passkey work properly in an Android application are not well documented. This post was aimed at highlighting those points so you can start your passkey journey in a more peaceful way.