Your First Android FIDO2 API

1. Introduction

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 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".

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...

  • Android device with a fingerprint sensor (even without a fingerprint sensor, screenlock can provide equivalent user verification functionality)
  • Android OS 7.0 or later with latest updates. Make sure to register a fingerprint (or screenlock).

2. Getting set up

Clone the Repository

Check out the GitHub repository.

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

$ git clone https://github.com/android/codelab-fido2.git

What are we going to implement?

  • Let users register a "user verifying platform authenticator" (the Android phone with fingerprint sensor itself will act as one).
  • Let users re-authenticate themselves to the app using their fingerprint.

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.

c2234c42ba8a6ef1.png

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. 9ef108869885e4ce.png
  3. Copy the project name on top left (you may modify it as you want). c91d0d59c61021a4.png
  4. Paste it to the .env file's HOSTNAME section in glitch. 889b55b1cf74b894.png

3. Associate your app and a website with the Digital Asset Links

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:

  • ANDROID_PACKAGENAME: Package name of your app (com.example.android.fido2)
  • ANDROID_SHA256HASH: SHA256 Hash of your signing certificate

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.

1062875cf11ffb95.png

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.

4. See how the app works now

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.

29351fb97062b43c.png

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".

bd9007614a9a3644.png

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".

d9caba817a0a99bd.png

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.

1cfcc6c884020e37.png

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.

e5a811bbc7cd7b30.png

5. Register a credential using a fingerprint

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.

37ce78fdf2759832.png

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 an ApiResult with all the PublicKeyCredentialCreationOptions that the client needs to generate a new credential.

We can then call getRegisterPendingIntent with the options. This FIDO2 API returns an Android PendingIntent to open a fingerprint dialog and generate a new credential, and we can return that PendingIntent to the caller.

The method will then look like below.

suspend fun registerRequest(): PendingIntent? {
  fido2ApiClient?.let { client ->
    try {
      val sessionId = dataStore.read(SESSION_ID)!!
      when (val apiResult = api.registerRequest(sessionId)) {
        ApiResult.SignedOutFromServer -> forceSignOut()
        is ApiResult.Success -> {
          if (apiResult.sessionId != null) {
            dataStore.edit { prefs ->
              prefs[SESSION_ID] = apiResult.sessionId
            }
          }
          val task = client.getRegisterPendingIntent(apiResult.data)
          return task.await()
        }
      }
    } catch (e: Exception) {
      Log.e(TAG, "Cannot call registerRequest", e)
    }
  }
  return null
}

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. Here, we'll use the createCredentialIntentLauncher member to launch the PendingIntent we got as the result of the previous step. This will open a dialog for credential generation.

binding.add.setOnClickListener {
  lifecycleScope.launch {
    val intent = viewModel.registerRequest()
    if (intent != null) {
      createCredentialIntentLauncher.launch(
        IntentSenderRequest.Builder(intent).build()
      )
    }
  }
}

Receive ActivityResult with the new Credential

Open HomeFragment.kt and find TODO(3).

This handleCreateCredentialResult method is called after the fingerprint dialog closes. If a credential was successfully generated, the data member of the ActivityResult will contain the credential information.

First, we have to extract a PublicKeyCredential from the data. The data Intent has an extra field of byte array with the key Fido.FIDO2_KEY_CREDENTIAL_EXTRA. You can use a static method in PublicKeyCredential called deserializeFromBytes to turn the byte array into a PublicKeyCredential object.

Next check if this credential object's response member is an AuthenticationErrorResponse. If it is, then there was an error generating the credential; otherwise, we can send credential to our backend.

The finished method will look like this:

private fun handleCreateCredentialResult(activityResult: ActivityResult) {
  val bytes = activityResult.data?.getByteArrayExtra(Fido.FIDO2_KEY_CREDENTIAL_EXTRA)
  when {
    activityResult.resultCode != Activity.RESULT_OK ->
      Toast.makeText(requireContext(), R.string.cancelled, Toast.LENGTH_LONG).show()
    bytes == null ->
      Toast.makeText(requireContext(), R.string.credential_error, Toast.LENGTH_LONG)
        .show()
    else -> {
      val credential = PublicKeyCredential.deserializeFromBytes(bytes)
      val response = credential.response
      if (response is AuthenticatorErrorResponse) {
        Toast.makeText(requireContext(), response.errorMessage, Toast.LENGTH_LONG)
          .show()
      } else {
        viewModel.registerResponse(credential)
      }
    }
  }
}

Call the server API: /auth/registerResponse

Open AuthRepository.kt and find TODO(4).

This registerResponse method is called after the UI successfully generated a new credential, and we want to send it back to the server.

The PublicKeyCredential 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 PublicKeyCredential object, take its rawId property and put it in a local string variable using toBase64.

Now we are ready to send the information to the server. Use api.registerResponse to call the server API and send back the response. The returned value contains a list of all the credentials registered on the server, including the new one.

Finally, we can save the results in our DataStore. The list of credentials should be saved with the key 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 LOCAL_CREDENTIAL_ID.

suspend fun registerResponse(credential: PublicKeyCredential) {
  try {
    val sessionId = dataStore.read(SESSION_ID)!!
    val credentialId = credential.rawId.toBase64()
    when (val result = api.registerResponse(sessionId, credential)) {
      ApiResult.SignedOutFromServer -> forceSignOut()
      is ApiResult.Success -> {
        dataStore.edit { prefs ->
          result.sessionId?.let { prefs[SESSION_ID] = it }
          prefs[CREDENTIALS] = result.data.toStringSet()
          prefs[LOCAL_CREDENTIAL_ID] = credentialId
        }
      }
    }
  } catch (e: ApiException) {
    Log.e(TAG, "Cannot call registerResponse", e)
  }
}

Run the app, and you will be able to click on the FAB and register a new credential.

7d64d9289c5a3cbc.png

6. Authenticate the user with a fingerprint

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 is redirected to HomeFragment.

Call the server API: /auth/signinRequest

Open AuthRepository.kt and find TODO(5).

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. The returned ApiResult contains PublicKeyCredentialRequestOptions.

With the PublicKeyCredentialRequestOptions, we can use FIDO2 API getSignIntent to create a PendingIntent to open the fingerprint dialog.

Finally, we can return the PendingIntent back to the UI.

suspend fun signinRequest(): PendingIntent? {
  fido2ApiClient?.let { client ->
    val sessionId = dataStore.read(SESSION_ID)!!
    val credentialId = dataStore.read(LOCAL_CREDENTIAL_ID)
    if (credentialId != null) {
      when (val apiResult = api.signinRequest(sessionId, credentialId)) {
        ApiResult.SignedOutFromServer -> forceSignOut()
        is ApiResult.Success -> {
          val task = client.getSignPendingIntent(apiResult.data)
          return task.await()
        }
      }
    }
  }
  return null
}

Open the fingerprint dialog for assertion

Open AuthFragment.kt and find TODO(6).

This is pretty much the same as what we did for registration. We can launch the fingerprint dialog with the signIntentLauncher member.

viewLifecycleOwner.lifecycleScope.launchWhenStarted {
  launch {
    viewModel.signinRequests.collect { intent ->
      signIntentLauncher.launch(
        IntentSenderRequest.Builder(intent).build()
      )
    }
  }
  launch {
    ...
  }
}

Handle the ActivityResult

Open AuthFragment.kt and find TODO(7).

Again, this is the same as what we did for registration. We can extract the PublicKeyCredential, check for an error, and pass it to the ViewModel.

private fun handleSignResult(activityResult: ActivityResult) {
  val bytes = activityResult.data?.getByteArrayExtra(Fido.FIDO2_KEY_CREDENTIAL_EXTRA)
  when {
    activityResult.resultCode != Activity.RESULT_OK ->
      Toast.makeText(requireContext(), R.string.cancelled, Toast.LENGTH_SHORT).show()
    bytes == null ->
      Toast.makeText(requireContext(), R.string.auth_error, Toast.LENGTH_SHORT)
        .show()
    else -> {
      val credential = PublicKeyCredential.deserializeFromBytes(bytes)
      val response = credential.response
      if (response is AuthenticatorErrorResponse) {
        Toast.makeText(requireContext(), response.errorMessage, Toast.LENGTH_SHORT)
          .show()
      } else {
        viewModel.signinResponse(credential)
      }
    }
  }
}

Call the server API: /auth/signinResponse

Open AuthRepository.kt and find TODO(8).

The PublicKeyCredential 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. The returned value contains a list of credentials.

At this point, the sign-in is successful. We have to store all the results in our DataStore. The list of credentials should be stored as StringSet with the key CREDENTIALS. The local credential ID we saved above should be stored as a string with key LOCAL_CREDENTIAL_ID.

Finally, we need to update the sign-in state so that the UI can redirect the user to the HomeFragment. This can be done by emitting a SignInState.SignedIn object to the SharedFlow named signInStateMutable. We also want to call refreshCredentials to fetch the user's credentials so that they will be listed in the UI.

suspend fun signinResponse(credential: PublicKeyCredential) {
  try {
    val username = dataStore.read(USERNAME)!!
    val sessionId = dataStore.read(SESSION_ID)!!
    val credentialId = credential.rawId.toBase64()
    when (val result = api.signinResponse(sessionId, credential)) {
      ApiResult.SignedOutFromServer -> forceSignOut()
      is ApiResult.Success -> {
        dataStore.edit { prefs ->
          result.sessionId?.let { prefs[SESSION_ID] = it }
          prefs[CREDENTIALS] = result.data.toStringSet()
          prefs[LOCAL_CREDENTIAL_ID] = credentialId
        }
        signInStateMutable.emit(SignInState.SignedIn(username))
        refreshCredentials()
      }
    }
  } catch (e: ApiException) {
    Log.e(TAG, "Cannot call registerResponse", e)
  }
}

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.

45f81419f84952c8.png

Congrats! You have now learned how to use FIDO2 API on Android for registration and sign-in.

7. Congratulations!

You have successfully finished the codelab - Your first Android FIDO2 API.

What you've learned

  • How to register a credential using a user verifying platform authenticator.
  • How to authenticate a user using a registered authenticator.
  • Available options for registering a new authenticator.
  • UX best practices for reauth using a biometric sensor.

Next step

  • Learn how to build similar experience in a website.

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

Resources

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