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.
You are going to work on your own version of the app.
- Go to the edit page of the website at https://glitch.com/edit/#!/webauthn-codelab.
- 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.
- Copy the project name on top left (you may modify it as you want).
- Paste it to the
.env
file'sHOSTNAME
section in glitch.
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.
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.
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.
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.
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.
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.
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.