FIDO2

Introduction

This guide explains how to implement the FIDO2 registration and authentication workflow on Android applications. You’ll find a detailed introduction about FIDO2 in our FIDO2 guide.

The following code samples and screenshots are issued from our example application using our SDK. Feel free to check out the Github project.

FIDO2 Prerequisites

To successfully enable FIDO2:

  1. Enable the WebAuthn feature on your ReachFive account.

    You may need to contact support to have this enabled if you do not see it on your ReachFive Console.
  2. From your ReachFive console:

    1. Navigate to Settings > WebAuthn.

    2. Configure the following:

      console webauthn settings
      1. Application name: the name of the application that is displayed to the user during the FIDO2 Registration or Authentication process. There are no particular restrictions on what you can use here.

      2. Allowed origins: the list of URLs that are allowed to make calls to the API endpoints provided by ReachFive to perform the FIDO2 Registration or Authentication process.

You also need
  • An Android device with a fingerprint sensor or a configured screen lock.

  • Android OS 7 or later.

    Make sure to register a fingerprint (or screenlock).
  • To install our SDK Core which exposes all of the FIDO2 methods.

  • To include the play-services-fido library as a dependency. To do this, go to your app folder and add the following to the build.gradle file under dependencies.

    dependencies {
        ...
        implementation "com.google.android.gms:play-services-fido:18.1.0"
        ...
    }

Initialize

You need to host a Digital Asset Links JSON file on https://<your_domain>/.well-known/assetlinks.json to allow the application to use FIDO2 APIs to register and sign credentials for that domain.

In our application example, we are:

[
  {
    "relation": ["delegate_permission/common.handle_all_urls","delegate_permission/common.get_login_creds"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.reach5.identity.sdk.demo", (1)
      "sha256_cert_fingerprints": [ (2)
        "AF:C8:F2:4C:AF:46:AA:5B:74:7B:8D:47:4D:82:A6:D1:FF:98:BF:A6:16:D0:D0:82:E8:9A:D0:8C:13:EB:B9:CE"
      ]
    }
  }
]

In the assetlinks.json file sample above:

  1. package_name matches the applicationId in the build.gradle file.

  2. sha256_cert_fingerprints matches the fingerprint of the signing key.

Refer to Android Studio’s Generate and register an upload certificate page to generate a public certificate.

Allowed origins

In the ReachFive Console, under Settings  WebAuthn you must allow:

  • the domain on which the Digital Asset Links file is hosted

  • the Facet ID

The Facet ID is the platform-specific identifier (URI). It matches the format android:apk-key-hash:<hash-of-apk-signing-cert> where hash-of-apk-signing-cert is the encoded version of the signing key from the application’s keystore.

You can compute the Facet ID with the command below:

keytool -list -v -keystore <your.keystore> | grep "SHA256: " | cut -d " " -f 3 | xxd -r -p | openssl base64 | sed 's/=//g'
webauthn settings

Signup

The user requests to register a new account for the first time. Users don’t need to provide a password, but instead provide an identifier like an email or mobile number as well as some personal data.

The data they provide in the signup process is reflected in the profile option in the signupWithWebAuthn method.
Subsection

Jump to the signup process

Here is a snapshot of the signup view displayed to the user.

webauthn signup

Signup process

Signup is a two-step process.

Each tab below shows the code needed for each step.
  1. The signupWithWebAuthn method initiates the FIDO2 signup process to retrieve a randomly generated challenge, the Relying Party information, and the user information from the ReachFive server. The options are then passed to the FIDO2 API which returns an Android Intent to open a fingerprint dialog and generates a new credential.

  2. The onLoginActivityResult method is called after the UI successfully generates a new credential. It sends the resulting data (which contains the information about this new credential) back to the ReachFive server and finalizes the FIDO2 signup process by returning the user’s authentication token.

  • signupWithWebAuthn

  • onSignupWithWebAuthnResult

package com.reach5.identity.sdk.demo

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.reach5.identity.sdk.core.ReachFive
import com.reach5.identity.sdk.core.models.requests.ProfileWebAuthnSignupRequest
import kotlinx.android.synthetic.main.webauthn_signup.*

// The activity controlling the view
class MainActivity : AppCompatActivity() {
    // The ReachFive client
    private lateinit var reach5: ReachFive

    // The WebAuthn identifier of the user
    private lateinit var webAuthnId: String

    companion object {
        // The domain of the origin call (it can be stored in an env file)
        const val ORIGIN = "https://dev-sandbox-268508.web.app"
        // The code of the FIDO2 signup request
        const val WEBAUTHN_SIGNUP_REQUEST_CODE = 3
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Define the action on the click of the "Signup" button
        signupWithWebAuthn.setOnClickListener {
            // Launch a pending FIDO2 task for signup
            this.reach5.signupWithWebAuthn(
                profile = ProfileWebAuthnSignupRequest(
                    email = signupWebAuthnEmail.text.toString(),
                    givenName = signupWebAuthnGivenName.text.toString(),
                    familyName = signupWebAuthnFamilyName.text.toString()
                ),
                origin = ORIGIN,
                friendlyName = signupWebAuthnNewFriendlyName.text.toString(),
                signupRequestCode = WEBAUTHN_SIGNUP_REQUEST_CODE,
                successWithWebAuthnId = { this.webAuthnId = it },
                failure = {
                    Log.d("ReachFive","Unable to signup with FIDO2: $it")
                }
            )
        }
    }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {

    // Method A
    // Tip: nothing happens if the request code does not concern ReachFive
    client.onLoginActivityResult(
        requestCode = requestCode,
        resultCode = resultCode,
        intent = data,
        success = { handleLoginSuccess(it) },
        failure = { reachFiveError ->
            Log.e(TAG, "Login error!", reachFiveError)
        },
        activity = this
    )

    // Method B
    val handler: ActivityResultHandler? = reach5.resolveResultHandler(requestCode, resultCode, data)
    when (handler) {
        is LoginResultHandler -> handler.handle(
            success = { handleLoginSuccess(it) },
            failure = { reachfiveError ->
                Log.e(TAG, "Login error!", reachfiveError)
            },
            activity = this
        )

        else -> { /* Not a login callback, or not ReachFive */ }
    }
}

Authentication

Once the user has a credential registered on the server and the device, they can use it to login easily with their identifier.

Subsection

Jump to the authentication process

Here is a snapshot of the login view displayed to the user.

webauthn login

Authentication process

Authentication is a two-step process.

Each tab below shows the code needed for each step.
  1. The loginWithWebAuthn method initiates the FIDO2 authentication process to retrieve the list of previously registered credentials and a challenge string from the server. This information is passed to the FIDO2 API which searches for a credential that matches the Relying Party ID and creates an Android Intent to open the fingerprint dialog for the user to consent for authentication.

  2. The onLoginActivityResult method is called after the UI successfully retrieves the existing credential. It returns the operation result to the ReachFive server and finalizes the FIDO2 authentication process by returning the user’s authentication token.

  • loginWithWebAuthn

  • onLoginActivityResult

package com.reach5.identity.sdk.demo

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.reach5.identity.sdk.core.ReachFive
import com.reach5.identity.sdk.core.models.requests.webAuthn.WebAuthnLoginRequest
import kotlinx.android.synthetic.main.webauthn_login.*

// The activity controlling the view
class MainActivity : AppCompatActivity() {
    // The ReachFive client
    private lateinit var reach5: ReachFive

    // The scope assigned to the user
    private val scope = setOf("openid", "email", "profile", "phone_number", "offline_access", "events", "full_write")

    companion object {
        // The domain of the origin call (it can be stored in an env file)
        const val ORIGIN = "https://dev-sandbox-268508.web.app"
        // The code of the FIDO2 authentication request
        const val WEBAUTHN_LOGIN_REQUEST_CODE = 2
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Define the action on the click of the "Login" button
        loginWithWebAuthn.setOnClickListener {
            val email: String = webAuthnEmail.text.toString()

            // Build the request according to the identifier provided by the user
            val webAuthnLoginRequest: WebAuthnLoginRequest = if (email.isNotEmpty()) {
                WebAuthnLoginRequest.EmailWebAuthnLoginRequest(
                    origin = ORIGIN,
                    email = email,
                    scope = scope
                )
            } else {
                WebAuthnLoginRequest.PhoneNumberWebAuthnLoginRequest(
                    origin = ORIGIN,
                    phoneNumber = webAuthnPhoneNumber.text.toString(),
                    scope = scope
                )
            }

            // Launch a pending FIDO2 task for authentication
            this.reach5.loginWithWebAuthn(
                loginRequest = webAuthnLoginRequest,
                loginRequestCode = WEBAUTHN_LOGIN_REQUEST_CODE,
                failure = {
                    Log.d("ReachFive","Unable to login with FIDO2: $it")
                }
            )
        }
    }
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {

    // Method A
    // Tip: nothing happens if the request code does not concern ReachFive
    client.onLoginActivityResult(
        requestCode = requestCode,
        resultCode = resultCode,
        intent = data,
        success = { handleLoginSuccess(it) },
        failure = { reachFiveError ->
            Log.e(TAG, "Login error!", reachFiveError)
        },
        activity = this
    )

    // Method B
    val handler: ActivityResultHandler? = reach5.resolveResultHandler(requestCode, resultCode, data)
    when (handler) {
        is LoginResultHandler -> handler.handle(
            success = { handleLoginSuccess(it) },
            failure = { reachfiveError ->
                Log.e(TAG, "Login error!", reachfiveError)
            },
            activity = this
        )

        else -> { /* Not a login callback, or not ReachFive */ }
    }
}

Manage devices

Once the user is authenticated, they can perform several actions to manage the registered devices such as:

Here is a snapshot of the view displayed to the user after a successful login.

webauthn devices

Register a new device

Registration is a two-step process.

Each tab below shows the code needed for each step.
  1. The addNewWebAuthnDevice method initiates the FIDO2 registration process to retrieve a randomly generated challenge, the Relying Party information, and the user information from the ReachFive server. The options are then passed to the FIDO2 API which returns an Android Intent to open a fingerprint dialog and generates a new credential.

    A user cannot register several credentials on the same device.
  2. The onAddNewWebAuthnDeviceResult method is called after the UI successfully generates a new credential. It sends the resulting data (which contains the information about this new credential) back to the ReachFive server and completes the FIDO2 registration process.

  • addNewWebAuthnDevice

  • onAddNewWebAuthnDeviceResult

package com.reach5.identity.sdk.demo

import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.reach5.identity.sdk.core.ReachFive
import com.reach5.identity.sdk.core.models.responses.AuthToken
import kotlinx.android.synthetic.main.webauthn_devices.*

// The activity controlling the view
class AuthenticatedActivity : AppCompatActivity() {
    // The ReachFive client
    private lateinit var reach5: ReachFive

    // The authentication token retrieved after the user has logged-in
    private lateinit var authToken: AuthToken

    companion object {
        // The domain of the origin call (it can be stored in an env file)
        const val ORIGIN = "https://dev-sandbox-268508.web.app"
        // The code of the FIDO2 registration request
        const val WEBAUTHN_REGISTER_REQUEST_CODE = 1
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_authenticated)

        // Fetch the authentication token from another activity
        this.authToken = intent.getParcelableExtra("AUTH_TOKEN")

        // Define the action on the click of the "Add new FIDO2 device" button
        addNewDevice.setOnClickListener {
            // Launch a pending FIDO2 task for registration
            this.reach5.addNewWebAuthnDevice(
                authToken = this.authToken,
                origin = ORIGIN,
                friendlyName = newFriendlyName.text.trim().toString(),
                registerRequestCode = WEBAUTHN_REGISTER_REQUEST_CODE,
                failure = {
                    Log.d("ReachFive", "Unable to add a new FIDO2 device: $it")
                }
            )
        }
    }
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)

    // Method A, using SDK helper:
    val handler = reach5.resolveResultHandler(requestCode, resultCode, data)
    if (handler is WebAuthnDeviceAddResult) {
        handler.handle(
            success = {
                showToast("New FIDO2 device registered")
                refreshDevicesDisplayed()
            },
            failure = {
                Log.d(TAG, "onAddNewWebAuthnDeviceResult error=$it")
                showErrorToast(it)
            }
        )
    }

    // Method B, directly calling method. Nothing happens if requestCode does not concern SDK:
    reach5.onAddNewWebAuthnDeviceResult(
        requestCode,
        data,
        success = {
            showToast("New FIDO2 device registered")
            refreshDevicesDisplayed()
        },
        failure = {
            Log.d(TAG, "onAddNewWebAuthnDeviceResult error=$it")
            showErrorToast(it)
        }
    )
}

Here are some snapshots of the FIDO2 dialogs displayed to the user to save a fingerprint.

webauthn registration

Other actions

The tabs below instruct you on listing and removing devices.

  • listWebAuthnDevices

  • removeWebAuthnDevice

Use the listWebAuthnDevices method to list the registered FIDO2 devices.

package com.reach5.identity.sdk.demo

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.reach5.identity.sdk.core.ReachFive
import com.reach5.identity.sdk.core.models.responses.AuthToken
import com.reach5.identity.sdk.core.models.responses.webAuthn.DeviceCredential

// The activity controlling the view
class AuthenticatedActivity : AppCompatActivity() {
    // The ReachFive client already instantiated in another activity
    private lateinit var reach5: ReachFive
    // The authentication token retrieved after the user has logged-in
    private lateinit var authToken: AuthToken
    // The list of the FIDO2 devices of the user
    private lateinit var devicesDisplayed: List<DeviceCredential>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_authenticated)

        /*...*/

        // Fetch the authentication token from another activity
        this.authToken = intent.getParcelableExtra("AUTH_TOKEN")
        // Initialize the list of devices
        this.devicesDisplayed = listOf()

        // Fetch the registered devices of the user from Reachfive server
        this.reach5.listWebAuthnDevices(
            authToken = authToken,
            success = {
                // Refresh the list of displayed devices
                this.devicesDisplayed = it
            },
            failure = {
                Log.d("ReachFive","Unable to list the FIDO2 devices: $it")
            }
        )
    }
}

+

Use the removeWebAuthnDevice method to delete a registered FIDO2 device.

package com.reach5.identity.sdk.demo

import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.reach5.identity.sdk.core.ReachFive
import com.reach5.identity.sdk.core.models.responses.AuthToken
import com.reach5.identity.sdk.core.models.responses.webAuthn.DeviceCredential

// The activity controlling the view
class AuthenticatedActivity : AppCompatActivity() {
    // The ReachFive client already instantiated in another activity
    private lateinit var reach5: ReachFive
    // The authentication token retrieved after the user has logged-in
    private lateinit var authToken: AuthToken
    // The list of the FIDO2 devices of the user
    private lateinit var devicesDisplayed: List<DeviceCredential>

    /*...*/

    private fun removeDevice(position: Int) {
        // The selected device
        val device = this.devicesDisplayed[position]

        // Delete the selected registered device from the ReachFive server
        reach5.removeWebAuthnDevice(
            authToken = authToken,
            deviceId = device.id,
            success = {
                Log.d("ReachFive", "The FIDO2 device '${device.friendlyName}' is removed")
            },
            failure = {
                Log.d("ReachFive", "Unable to remove the FIDO2 device '${device.friendlyName}': $it")
            }
        )
    }
}

+