您的首个 Android FIDO2 API

1. 简介

FIDO2 API 是什么?

通过 FIDO2 API,Android 应用可以创建和使用安全系数高且经过认证的基于公钥的凭据,以便对用户进行身份验证。该 API 提供了一个 WebAuthn 客户端实现,它支持使用 BLE、NFC 和 USB 漫游身份验证器(安全密钥)以及平台身份验证器,用户可使用其指纹或屏幕锁定功能来进行身份验证。

构建内容...

在此 Codelab 中,您将使用指纹传感器构建一个具有简单的重新身份验证功能的 Android 应用。“重新验证”是指用户登录应用,然后在用户切换回您的应用或尝试访问应用的重要部分时重新进行身份验证。后一种情况也称为“分步身份验证”。

学习内容…

您将学习如何调用 Android FIDO2 API 以及您可以提供的选项,以满足不同场合的需求。您还将了解与重新验证相关的最佳实践。

所需条件…

  • 配有指纹传感器的 Android 设备(即使没有指纹传感器,屏幕锁定也能提供等效的用户验证功能)
  • 安装了最新更新的 Android OS 7.0 或更高版本。请务必注册指纹(或屏幕锁定)。

2. 准备工作

克隆代码库

查看 GitHub 代码库。

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

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

我们要实现什么?

  • 允许用户注册“用户验证平台身份验证器”(带有指纹传感器的 Android 手机将充当一个传感器)。
  • 允许用户使用指纹向应用重新验证身份。

您可以点击此处预览将要构建的应用。

启动 Codelab 项目

完成后的应用会向服务器发送请求 (https://webauthn-codelab.glitch.me)。您可以前往该网站试用同一应用的网页版。

c2234c42ba8a6ef1.png

您将开发自己的应用版本。

  1. 前往网站的修改页面:https://glitch.com/edit/#!/webauthn-codelab
  2. 找到“混剪以进行编辑”按钮。按下按钮,即可“创建分支”继续使用您自己的版本和新的项目网址。9ef108869885e4ce
  3. 复制左上角的项目名称(您可以根据需要进行修改)。c91d0d59c61021a4.png
  4. 将其粘贴到 .env 文件的 HOSTNAME 部分(出现故障)。889b55b1cf74b894

3. 通过 Digital Asset Links 将应用与网站相关联

如需在 Android 应用中使用 FIDO2 API,请将其与网站相关联,并在网站之间共享凭据。为此,请使用 Digital Asset Links。您可以通过以下方式声明关联:在网站上托管 Digital Asset Links JSON 文件,并将指向 Digital Asset Links 文件的链接添加到应用的清单中。

在您的网域托管 .well-known/assetlinks.json

您可以创建一个 JSON 文件并将其放在 .well-known/assetlinks.json 中,从而定义您的应用与网站之间的关联。幸运的是,我们有一个可以自动显示 assetlinks.json 文件的服务器代码,只需在故障中向 .env 文件添加以下环境参数即可:

  • ANDROID_PACKAGENAME:应用的软件包名称 (com.example.android.fido2)
  • ANDROID_SHA256HASH:签名证书的 SHA256 哈希

如需获取开发者签名证书的 SHA256 哈希值,请使用以下命令。调试密钥库的默认密码为“android”。

$ keytool -exportcert -list -v -alias androiddebugkey -keystore ~/.android/debug.keystore

访问 https://<your-project-name>.glitch.me/.well-known/assetlinks.json 后,您应该会看到如下所示的 JSON 字符串:

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

在 Android Studio 中打开项目

点击“Open an existing Android Studio project”(打开现有 Android Studio 项目)。

选择“android”文件夹签出。

1062875cf11ffb95

将该应用与您的混剪作品相关联

打开 gradle.properties 文件。在文件底部,将主机网址更改为您刚刚创建的 Glitch 混剪内容。

// ...

# The URL of the server
host=https://<your-project-name>.glitch.me

此时,您的 Digital Asset Links 配置应该已设置完毕。

4. 了解该应用现在的运作方式

我们先来看看该应用现在的工作原理。请务必在运行配置组合框中选择“app-start”。点击“运行”(组合框旁边的绿色三角形),以便在已连接的 Android 设备上启动应用。

29351fb97062b43c

启动应用后,您会看到供您输入用户名的屏幕。这是UsernameFragment。出于演示目的,应用和服务器接受任何用户名。只需输入内容并按“下一步”即可。

bd9007614a9a3644.png

您看到的下一个屏幕是 AuthFragment。用户可以使用密码登录。我们稍后将在此处添加使用 FIDO2 登录的功能。同样,出于演示目的,应用和服务器接受任何密码。只需输入内容,然后按“登录”即可。

d9caba817a0a99bd.png

这是该应用的最后一个屏幕,即 HomeFragment。目前,您在此处仅看到一个空的凭据列表。按“重新验证”返回到 AuthFragment。按“退出”返回到 UsernameFragment。带“+”的悬浮操作按钮标志现在不会执行任何操作,但它会启动

实施 FIDO2 注册流程后,您需要提供新凭据。

1cfcc6c884020e37

在开始编码之前,先介绍一个有用的技巧。在 Android Studio 中,按“TODO”。该标签页会显示此 Codelab 中所有 TODO 的列表。在下一部分中,我们将从第一个 TODO 开始。

e5a811bbc7cd7b30.png

5. 使用指纹注册凭据

若要启用指纹身份验证,您首先需要注册由用户验证平台身份验证器(一种设备嵌入式身份验证器,使用指纹传感器等生物识别技术验证用户)生成的凭据。

37ce78fdf2759832

如上一部分所述,悬浮操作按钮现在不起作用。我们来看看如何注册新凭据。

调用服务器 API:/auth/registerRequest

打开 AuthRepository.kt 并找到 TODO(1)。

其中,registerRequest 是按下 FAB 时调用的方法。我们希望使此方法调用服务器 API /auth/registerRequest。该 API 会返回一个 ApiResult,其中包含客户端生成新凭据所需的所有 PublicKeyCredentialCreationOptions

然后,我们可以使用这些选项来调用 getRegisterPendingIntent。此 FIDO2 API 会返回一个 Android PendingIntent 以打开指纹对话框并生成新的凭据,然后我们可以将该 PendingIntent 返回给调用方。

该方法将如下所示。

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
}

打开指纹对话框进行注册

打开 HomeFragment.kt 并找到 TODO(2)。

这是界面从 AuthRepository 获取返回 intent 的位置。在这里,我们将使用 createCredentialIntentLauncher 成员启动通过上一步获得的 PendingIntent。这将打开一个用于生成凭据的对话框。

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

使用新凭据接收 ActivityResult

打开 HomeFragment.kt 并找到 TODO(3)。

指纹对话框关闭后,系统会调用此 handleCreateCredentialResult 方法。如果成功生成凭据,ActivityResult 的 data 成员将包含凭据信息。

首先,我们必须从 data 中提取 PublicKeyCredential。数据 intent 有一个额外的字节数组字段,其键为 Fido.FIDO2_KEY_CREDENTIAL_EXTRA。您可以在 PublicKeyCredential 中使用一个名为 deserializeFromBytes 的静态方法,将字节数组转换为 PublicKeyCredential 对象。

接下来,检查此凭据对象的 response 成员是否为 AuthenticationErrorResponse。如果正确,则表示生成凭据时出错;否则,我们可以将凭据发送到后端。

完成后的方法将如下所示:

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

调用服务器 API:/auth/registerResponse

打开 AuthRepository.kt 并找到 TODO(4)。

在界面成功生成新凭据且我们希望将其发送回服务器后,系统会调用此 registerResponse 方法。

PublicKeyCredential 对象包含有关新生成的凭据的信息。现在,我们需要记住本地密钥的 ID,以便将其与在服务器上注册的其他密钥区分开来。在 PublicKeyCredential 对象中,获取其 rawId 属性,并使用 toBase64 将其放入本地字符串变量中。

现在,我们可以将信息发送到服务器了。使用 api.registerResponse 调用服务器 API 并发回响应。返回的值包含在服务器上注册的所有凭据的列表,包括新凭据。

最后,我们可以将结果保存在 DataStore 中。凭据列表应使用 CREDENTIALS 密钥以 StringSet 的形式保存。您可以使用 toStringSet 将凭据列表转换为 StringSet

此外,我们还会使用键 LOCAL_CREDENTIAL_ID 保存凭据 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)
  }
}

运行应用,您将能够点击 FAB 并注册新凭据。

7d64d9289c5a3cbc.png

6. 使用指纹验证用户身份

现在,我们在应用和服务器上注册了凭据。我们现在可以使用它来让用户登录。我们正在为 AuthFragment 添加指纹登录功能。当用户进入该页面时,系统会显示指纹对话框。身份验证成功后,系统会将用户重定向到 HomeFragment

调用服务器 API:/auth/signinRequest

打开 AuthRepository.kt 并找到 TODO(5)。

打开 AuthFragment 时,系统会调用此 signinRequest 方法。在这里,我们想要请求服务器,看看是否可以让用户使用 FIDO2 登录。

首先,我们必须从服务器中检索 PublicKeyCredentialRequestOptions。使用 api.signInRequest 调用服务器 API。返回的 ApiResult 包含 PublicKeyCredentialRequestOptions

借助 PublicKeyCredentialRequestOptions,我们可以使用 FIDO2 API getSignIntent 创建 PendingIntent 以打开指纹对话框。

最后,我们可以将 PendingIntent 返回给界面。

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
}

打开指纹对话框以进行断言

打开 AuthFragment.kt 并找到 TODO(6)。

这与我们注册时执行的操作几乎相同。我们可以使用 signIntentLauncher 成员启动指纹对话框。

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

处理 ActivityResult

打开 AuthFragment.kt 并找到 TODO(7)。

同样,这与我们在注册时执行的操作相同。我们可以提取 PublicKeyCredential,检查是否存在错误,并将其传递给 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)
      }
    }
  }
}

调用服务器 API:/auth/signinResponse

打开 AuthRepository.kt 并找到 TODO(8)。

PublicKeyCredential 对象中包含 keyHandle 的凭据 ID。正如我们在注册流程中所做的那样,我们要将此代码保存到本地字符串变量中,以便稍后存储。

现在,我们可以使用 api.signinResponse 来调用服务器 API 了。返回的值包含一个凭据列表。

此时,登录成功。我们必须将所有结果存储在 DataStore 中。凭据列表应存储为带有 CREDENTIALS 键的 StringSet。我们在上面保存的本地凭据 ID 应存储为带有 LOCAL_CREDENTIAL_ID 键的字符串。

最后,我们需要更新登录状态,以便界面可以将用户重定向到 HomeFragment。这可以通过向名为 signInStateMutable 的 SharedFlow 发出 SignInState.SignedIn 对象来实现。我们还需要调用 refreshCredentials 来获取用户的凭据,以便将其列在界面中。

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

运行应用,然后点击“Reauth”以打开 AuthFragment。现在,您应该会看到一个指纹对话框,提示您使用指纹登录。

45f81419f84952c8.png

恭喜!您现在已经了解了如何在 Android 上使用 FIDO2 API 进行注册和登录。

7. 恭喜!

您已成功完成此 Codelab - 您的第一个 Android FIDO2 API

您学到的内容

  • 如何使用用户验证平台身份验证器来注册凭据。
  • 如何使用已注册的身份验证器对用户进行身份验证。
  • 用于注册新身份验证器的可用选项。
  • 使用生物识别传感器重新进行身份验证的用户体验最佳做法。

下一步

  • 了解如何在网站中打造相似的体验。

您可以尝试学习您的第一个 WebAuthn Codelab!

资源

特别感谢 FIDO 联盟的 Yuriy Ackermann 提供的帮助。