The FIDO2 API allows Android applications to create and use strong, attested public key-based credentials for the purpose of authenticating users. The API provides a WebAuthn Client implementation, which supports the use of BLE, NFC, and USB roaming authenticators (security keys) as well as a platform authenticator, which allows the user to authenticate using their fingerprint or screenlock.
In this codelab, you are going to build an Android app with a simple re-authentication functionality using fingerprint sensor. "Re-authentication" is when a user signs in to an app, then re-authenticates when they switch back to your app, or when trying to access an important section of your app. The latter case is also referred to as "step-up authentication".
You will learn how to call the Android FIDO2 API and options you can provide in order to cater various occasions. You will also learn re-auth specific best practices.
Check out the GitHub repository.
https://github.com/googlecodelabs/fido2-codelab
$ git clone git@github.com:googlecodelabs/fido2-codelab.git
You can preview what you are going to build from here.
The completed app sends requests to a server at https://webauthn-codelab.glitch.me. You may try web version of the same app there.
You are going to work on your own version of the app.
.env
file's HOSTNAME
section in glitch.To use FIDO2 API on an Android app, associate it with a website and share credentials between them. To do so, leverage the Digital Asset Links. You can declare associations by hosting a Digital Asset Links JSON file on your website, and adding a link to the Digital Asset Link file to your app's manifest.
.well-known/assetlinks.json
at your domainYou can define an association between your app and the website by creating a JSON file and put it at .well-known/assetlinks.json
. Luckily, we have a server code that displays assetlinks.json
file automatically, just by adding following environment params to the .env
file in glitch:
ANDROID_PACKAGENAME
: Package name of your app (com.example.android.fido2)ANDROID_SHA256HASH
: SHA256 Hash of your signing certificateIn order to get the SHA256 hash of your developer signing certificate, use the command below. The default password of the debug keystore is "android".
$ keytool -exportcert -list -v -alias androiddebugkey -keystore ~/.android/debug.keystore
By accessing https://<your-project-name>.glitch.me/.well-known/assetlinks.json
, you should see a JSON string like this:
[{
"relation": ["delegate_permission/common.handle_all_urls", "delegate_permission/common.get_login_creds"],
"target": {
"namespace": "web",
"site": "https://<your-project-name>.glitch.me"
}
}, {
"relation": ["delegate_permission/common.handle_all_urls", "delegate_permission/common.get_login_creds"],
"target": {
"namespace": "android_app",
"package_name": "com.example.android.fido2",
"sha256_cert_fingerprints": ["DE:AD:BE:EF:..."]
}
}]
Click "Open an existing Android Studio project" on the welcome screen of Android Studio.
Choose the "android" folder inside the repository check out.
Open gradle.properties
file. At the bottom of the file, change the host URL to the Glitch remix you just created.
// ...
# The URL of the server
host=https://<your-project-name>.glitch.me
At this point, your Digital Asset Links configuration should be all set.
Let's start by checking out how the app works now. Make sure to select "app-start" in the run configuration combobox. Click "Run" (the green triangular next to the combobox) to launch the app on your connected Android device.
When you launch the app you'll see the screen to type your username. This is UsernameFragment
. For the purpose of demonstration, the app and the server accept any username. Just type something and press "Next".
The next screen you see is AuthFragment
. This is where the user can sign in with a password. We will later add a feature to sign in with FIDO2 here. Again, for the purpose of demonstration the app and the server accept any password. Just type something and press "Sign In".
This is the last screen of this app, HomeFragment
. For now, you only see an empty list of credentials here. Pressing "Reauth" takes you back to AuthFragment
. Pressing "Sign Out" takes you back to UsernameFragment
. The floating action button with "+" sign doesn't do anything now, but it will initiate registration of a
new credential once you have implemented the FIDO2 registration flow.
Before starting to code, here's a useful technique. On Android Studio, press "TODO" at the bottom. It will show a list of all the TODOs in this codelab. We'll start with the first TODO in the next section.
In order to enable authentication using a fingerprint, you'll first need to register a credential generated by a user verifying platform authenticator - a device-embedded authenticator that verifies the user using biometrics, such as a fingerprint sensor.
As we have seen in the previous section, the floating action button doesn't do anything now. Let's see how we can register a new credential.
/auth/registerRequest
Open AuthRepository.kt
and find TODO(1).
Here, registerRequest
is the method that is called when the FAB is pressed. We'd like to make this method call the server API /auth/registerRequest
. The API returns all the PublicKeyCredentialCreationOptions
that the client needs to generate a new credential. It also returns a challenge as a string. We need this for a subsequent API call of /auth/registerResponse, so let's save this in a local property.
We can then call getRegisterIntent with said options. This FIDO2 API returns an Android Intent to open a fingerprint dialog and generate a new credential.
Now we have the Intent, all we have to do is to pass it back to our UI so it can proceed to show the fingerprint dialog. The method returns a MutableLiveData
. We can simply post the Intent as the LiveData's value.
The method will then look like something below.
fun registerRequest(processing: MutableLiveData<Boolean>): LiveData<Fido2PendingIntent> {
val result = MutableLiveData<Fido2PendingIntent>()
executor.execute {
fido2ApiClient?.let { client ->
processing.postValue(true)
try {
val token = prefs.getString(PREF_TOKEN, null)!!
// Call the API.
val (options, challenge) = api.registerRequest(token)
// Save the challenge.
lastKnownChallenge = challenge
// Use getRegisterIntent to get an Intent to
// open the fingerprint dialog.
val task: Task<Fido2PendingIntent> = client.getRegisterIntent(options)
// Pass the Intent back to the UI.
result.postValue(Tasks.await(task))
} catch (e: Exception) {
Log.e(TAG, "Cannot call registerRequest", e)
} finally {
processing.postValue(false)
}
}
}
return result
}
Open HomeFragment.kt
and find TODO(2).
This is where the UI gets the Intent back from our AuthRepository
. The returned object has a convenient method called launchPendingIntent
. Calling it will open a dialog for credential generation.
binding.add.setOnClickListener {
viewModel.registerRequest().observeOnce(requireActivity()) { intent ->
val a = activity
if (intent.hasPendingIntent() && a != null) {
try {
// Launch the fingerprint dialog.
intent.launchPendingIntent(a, MainActivity.REQUEST_FIDO2_REGISTER)
} catch (e: IntentSender.SendIntentException) {
Log.e(TAG, "Error launching pending intent for register request", e)
}
}
}
}
/auth/registerResponse
Open AuthRepository.kt
and find TODO(3).
This registerReponse
method is called after the UI successfully generated a new credential. The parameter data
has all the information about this new credential. We want to send it back to the server.
First, we have to extract an AuthenticatorAttestationResponse
from the data
. The data Intent has an extra field of byte array with the key Fido.FIDO2_KEY_RESPONSE_EXTRA
. You can use a static method in AuthenticatorAttestationResponse
called deserializeFromBytes
to turn the byte array into an AuthenticatorAttestationResponse
object.
The AuthenticatorAttestationResponse
object has information about the newly generated credential inside. We now want to remember the ID of our local key so we can distinguish it from other keys registered on the server. In the AuthenticatorAttestationResponse
object, take its keyHandle
property and save it in a local string variable as using toBase64
.
Now we are ready to send the information to the server. Use api.registerReponse
to call the server API and send the token, the challenge string and the response. The returned value is a list of all the credentials registered on the server, including the new one.
Finally, we can save the results in our SharedPreferences
. The list of credentials should be saved with the key PREF_CREDENTIALS
as a StringSet
. You can use toStringSet
to convert the list of credentials into a StringSet
.
In addition, we save the credential ID with the key PREF_LOCAL_CREDENTIAL_ID
.
fun registerResponse(data: Intent, processing: MutableLiveData<Boolean>) {
executor.execute {
processing.postValue(true)
try {
val token = prefs.getString(PREF_TOKEN, null)!!
val challenge = lastKnownChallenge!!
// Extract the AuthenticatorAttestationResponse.
val response = AuthenticatorAttestationResponse.deserializeFromBytes(
data.getByteArrayExtra(Fido.FIDO2_KEY_RESPONSE_EXTRA)
)
// Memorize the credential ID.
val credentialId = response.keyHandle.toBase64()
// Call /auth/registerResponse.
val credentials = api.registerResponse(token, challenge, response)
// Save the results.
prefs.edit {
putStringSet(PREF_CREDENTIALS, credentials.toStringSet())
putString(PREF_LOCAL_CREDENTIAL_ID, credentialId)
}
} catch (e: ApiException) {
Log.e(TAG, "Cannot call registerResponse", e)
} finally {
processing.postValue(false)
}
}
}
Run the app, and you will be able to click on the FAB and register a new credential.
We now have a credential registered on the app and the server. We can now use it to let the user sign in. We are adding fingerprint sign-in feature to AuthFragment
. When a user lands on it, it shows a fingerprint dialog. When the authentication succeeds, the user to redirected to HomeFragment
.
/auth/signinRequest
Open AuthRepository.kt
and find TODO(4).
This signinRequest
method is called when AuthFragment
is opened. Here, we want to request the server and see if we can let the user sign in with FIDO2.
First, we have to retrieve PublicKeyCredentialRequestOptions
from the server. Use api.signInRequest
to call the server API. It returns two values, PublicKeyCredentialRequestOptions
and a challenge string. We will use the challenge string later, so let's save it in a property.
With the PublicKeyCredentialRequestOptions
, we can use FIDO2 API getSignIntent
to create an Intent to open the fingerprint dialog.
Finally, we can pass the Intent back to the UI.
fun signinRequest(processing: MutableLiveData<Boolean>): LiveData<Fido2PendingIntent> {
val result = MutableLiveData<Fido2PendingIntent>()
executor.execute {
fido2ApiClient?.let { client ->
processing.postValue(true)
try {
val username = prefs.getString(PREF_USERNAME, null)!!
val credentialId = prefs.getString(PREF_LOCAL_CREDENTIAL_ID, null)
// Retrieve sign-in options from the server.
val (options, challenge) = api.signinRequest(username, credentialId)
// Save the challenge string.
lastKnownChallenge = challenge
// Create an Intent to open the fingerprint dialog.
val task = client.getSignIntent(options)
// Pass the Intent back to the UI.
result.postValue(Tasks.await(task))
} finally {
processing.postValue(false)
}
}
}
return result
}
Open AuthFragment.kt
and find TODO(5).
This is pretty much the same as what we did for registration. We can launch the fingerprint dialog with the launchPendingIntent
method.
viewModel.signinIntent.observeOnce(requireActivity()) { intent ->
val a = activity
if (intent.hasPendingIntent() && a != null) {
try {
// Launch the fingerprint dialog.
intent.launchPendingIntent(a, MainActivity.REQUEST_FIDO2_SIGNIN)
} catch (e: IntentSender.SendIntentException) {
Log.e(TAG, "Error launching pending intent for signin request", e)
}
}
}
/auth/signinResponse
Open AuthRepository.kt
and find TODO(6).
First, we have to extract an AuthenticatorAssertionResponse
from the method parameter data
. You can use AuthenticatorAssertionResponse.deserializeFromBytes
to convert the byte array extra stored in data
with the key Fido.FIDO2_KEY_RESPONSE_EXTRA
.
The response object has a credential ID in it as keyHandle
. Just like we did in the registration flow, let's save this in a local string variable so we can store it later.
We are now ready to call the server API with api.signinResponse
. It will return two values, a list of credentials, and a sign-in token.
At this point, the sign-in is successful. We have to store all the results in our SharedPreferences
. The sign-in token should be stored as a string with key PREF_TOKEN
. The list of credentials should be stored as StringSet with the key PREF_CREDENTIALS
. The local credential ID we saved above should be stored as a string with key PREF_LOCAL_CREDENTIAL_ID
.
Finally, we have to let the UI know that the sign-in has succeeded so that the user is redirected to the home screen. This can be done by calling invokeSignInStateListeners
. Pass SignInState.SignedIn
as an argument.
fun signinResponse(data: Intent, processing: MutableLiveData<Boolean>) {
executor.execute {
processing.postValue(true)
try {
val username = prefs.getString(PREF_USERNAME, null)!!
val challenge = lastKnownChallenge!!
// Extract the AuthenticatorAssertionResponse.
val response = AuthenticatorAssertionResponse.deserializeFromBytes(
data.getByteArrayExtra(Fido.FIDO2_KEY_RESPONSE_EXTRA)
)
// Save the credential ID.
val credentialId = response.keyHandle.toBase64()
// Send the information to the server
val (credentials, token) = api.signinResponse(username, challenge, response)
// Store the results.
prefs.edit(commit = true) {
putString(PREF_TOKEN, token)
putStringSet(PREF_CREDENTIALS, credentials.toStringSet())
putString(PREF_LOCAL_CREDENTIAL_ID, credentialId)
}
// Let the UI know that the sign-in succeeded.
invokeSignInStateListeners(SignInState.SignedIn(username, token))
} catch (e: ApiException) {
Log.e(TAG, "Cannot call registerResponse", e)
} finally {
processing.postValue(false)
}
}
}
Run the app and click on "Reauth" to open AuthFragment
. You should now see a fingerprint dialog prompting you to sign in with your fingerprint.
Congrats! You have now learned how to use FIDO2 API on Android for registration and sign-in.
You have successfully finished the codelab - Your first Android FIDO2 API.
You can learn it by trying out the Your first WebAuthn codelab!
Special thanks to Yuriy Ackermann from FIDO Alliance for your help.