What is the FIDO2 API?

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.

What you'll build...

In this codelab, you are going to build an Android app with a simple re-authentication functionality using fingerprint sensor. "Re-authentication" is a concept where a user signs into 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".

What you'll learn...

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.

What you'll need...

Clone the Repository

Check out the GitHub repository.

https://github.com/googlecodelabs/fido2-codelab

$ git clone git@github.com:googlecodelabs/fido2-codelab.git

What are we going to implement?

You can preview what you are going to build from here.

Start your codelab project

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.

  1. Go to the edit page of the website at https://glitch.com/edit/#!/webauthn-codelab.
  2. Find "Remix to Edit" button at the top right corner. By pressing the button, you can "fork" the code and continue with your own version along with a new project URL.
  3. Copy the project name on top left (you may modify it as you want).
  4. Paste it to the .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.

Host .well-known/assetlinks.json at your domain

You 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:

In 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:..."]
  }
}]

Open the project in Android Studio

Click "Open an existing Android Studio project" on the welcome screen of Android Studio.

Choose the "android" folder inside the repository check out.

Associate the app with your remix

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.

Call the server API: /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 the fingerprint dialog for registration

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)
                    }
                }
            }
        }

Call the server API: /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.

Call the server API: /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 the fingerprint dialog for assertion

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)
                }
            }
        }

Call the server API: /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.

What you've learned

Next step

You can learn it by trying out the Your first WebAuthn codelab!

Resources

Special thanks to Yuriy Ackermann from FIDO Alliance for your help.