从 FIDO2 迁移到 Credential Manager

凭借对通行密钥 (passkeys)、联合登录以及第三方身份验证提供商的支持,Credential Manager 现已成为 Android 上推荐使用的身份验证 API。它提供了一个安全便捷的环境,允许用户同步和管理其凭据。对于目前使用本地 FIDO2 凭据的开发者,建议通过集成 Credential Manager API 来更新应用,以支持通行密钥身份验证。本文档介绍了如何将项目从 FIDO2 迁移到 Credential Manager。

从 FIDO2 迁移到 Credential Manager 的原因

在大多数情况下,您应该将 Android 应用的身份验证提供商迁移到 Credential Manager。迁移到 Credential Manager 的原因包括:

  • 支持通行密钥:Credential Manager 支持通行密钥,这是一种全新的、无密码的身份验证机制,比密码更安全且更易于使用。
  • 多种登录方式:Credential Manager 支持多种登录方式,包括密码、通行密钥和联合登录方式。无论用户偏好哪种身份验证方式,这都让用户能更轻松地登录您的应用。
  • 支持第三方凭据提供商:在 Android 14 及更高版本上,Credential Manager 支持多种第三方凭据提供商。这意味着您的用户可以使用来自其他提供商的现有凭据登录您的应用。
  • 一致的用户体验:Credential Manager 为跨应用和登录机制的身份验证提供了更一致的用户体验。这使用户更容易理解和使用您应用的身份验证流程。

要开始从 FIDO2 迁移到 Credential Manager,请按照以下步骤操作。

更新依赖项

  1. 将项目中 build.gradle 的 Kotlin 插件更新至 1.8.10 或更高版本。

      plugins {
        //…
          id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
        //…
      }
    
  2. 在项目的 build.gradle 中,更新您的依赖项以使用 Credential Manager 和 Play Services Authentication 库的最新版本

      dependencies {
        // ...
        // Credential Manager:
        implementation 'androidx.credentials:credentials:<latest-version>'
    
        // Play Services Authentication:
        // Optional - needed for credentials support from play services, for devices running
        // Android 13 and below:
        implementation 'androidx.credentials:credentials-play-services-auth:<latest-version>'
        // ...
      }
    
  3. 用 Credential Manager 初始化替换 FIDO 初始化。在您用于创建通行密钥和登录方法的类中添加此声明:

    val credMan = CredentialManager.create(context)
    

创建通行密钥

在用户可以使用通行密钥登录之前,您需要创建一个新的通行密钥,将其与用户的账户关联,并将该通行密钥的公钥存储在您的服务器上。通过更新注册函数调用,为您的应用设置此功能。

图 1. 此图显示了使用 Credential Manager 创建通行密钥时,应用与服务器之间的数据交换方式。
  1. 要获取通行密钥创建期间发送到 createCredential() 方法所需的参数,请按照 WebAuthn 规范所述,将 name("residentKey").value("required") 添加到您的 registerRequest() 服务器调用中。

    suspend fun registerRequest() {
        // ...
        val call = client.newCall(
            Builder()
                .method(
                    "POST",
                    jsonRequestBody {
                        name("attestation").value("none")
                        name("authenticatorSelection").objectValue {
                            name("residentKey").value("required")
                        }
                    }
                ).build()
        )
        // ...
    }
    
  2. registerRequest() 及其所有子函数的 return 类型设置为 JSONObject

    suspend fun registerRequest(sessionId: String): ApiResult<JSONObject> {
        val call = client.newCall(
            Builder()
                .url("$BASE_URL/<your api url>")
                .addHeader("Cookie", formatCookie(sessionId))
                .method(
                    "POST",
                    jsonRequestBody {
                        name("attestation").value("none")
                        name("authenticatorSelection").objectValue {
                            name("authenticatorAttachment").value("platform")
                            name("userVerification").value("required")
                            name("residentKey").value("required")
                        }
                    }
                ).build()
        )
        val response = call.await()
        return response.result("Error calling the api") {
            parsePublicKeyCredentialCreationOptions(
                body ?: throw ApiException("Empty response from the api call")
            )
        }
    }
    
  3. 安全地移除视图中处理意图启动器 (intent launcher) 和活动结果 (activity result) 调用的所有方法。

  4. 由于 registerRequest() 现在返回一个 JSONObject,您无需创建 PendingIntent。请用 JSONObject 替换返回的 intent。更新您的 intent 启动器调用,使其调用 Credential Manager API 中的 createCredential()。调用 createCredential() API 方法。

    suspend fun createPasskey(
        activity: Activity,
        requestResult: JSONObject
    ): CreatePublicKeyCredentialResponse? {
        val request = CreatePublicKeyCredentialRequest(requestResult.toString())
        var response: CreatePublicKeyCredentialResponse? = null
        try {
            response = credMan.createCredential(
                request = request as CreateCredentialRequest,
                context = activity
            ) as CreatePublicKeyCredentialResponse
        } catch (e: CreateCredentialException) {
    
            showErrorAlert(activity, e)
    
            return null
        }
        return response
    }
    
  5. 调用成功后,将响应发送回服务器。此调用的请求和响应与 FIDO2 实现类似,因此无需进行任何更改。

使用通行密钥进行身份验证

设置好通行密钥创建功能后,您可以配置应用以允许用户使用其通行密钥登录和进行身份验证。为此,您需要更新身份验证代码以处理 Credential Manager 的结果,并实现一个通过通行密钥进行身份验证的函数。

图 2. Credential Manager 的通行密钥身份验证流程。
  1. 您向服务器发出的登录请求调用(用于获取发送到 getCredential() 请求所需的信息)与 FIDO2 实现相同。无需更改。
  2. 与注册请求调用类似,返回的响应采用 JSONObject 格式。

    /**
     * @param sessionId The session ID to be used for the sign-in.
     * @param credentialId The credential ID of this device.
     * @return a JSON object.
     */
    suspend fun signinRequest(): ApiResult<JSONObject> {
        val call = client.newCall(
            Builder().url(
                buildString {
                    append("$BASE_URL/signinRequest")
                }
            ).method("POST", jsonRequestBody {})
                .build()
        )
        val response = call.await()
        return response.result("Error calling /signinRequest") {
            parsePublicKeyCredentialRequestOptions(
                body ?: throw ApiException("Empty response from /signinRequest")
            )
        }
    }
    
    /**
     * @param sessionId The session ID to be used for the sign-in.
     * @param response The JSONObject for signInResponse.
     * @param credentialId id/rawId.
     * @return A list of all the credentials registered on the server,
     * including the newly-registered one.
     */
    suspend fun signinResponse(
        sessionId: String,
        response: JSONObject,
        credentialId: String
    ): ApiResult<Unit> {
    
        val call = client.newCall(
            Builder().url("$BASE_URL/signinResponse")
                .addHeader("Cookie", formatCookie(sessionId))
                .method(
                    "POST",
                    jsonRequestBody {
                        name("id").value(credentialId)
                        name("type").value(PUBLIC_KEY.toString())
                        name("rawId").value(credentialId)
                        name("response").objectValue {
                            name("clientDataJSON").value(
                                response.getString("clientDataJSON")
                            )
                            name("authenticatorData").value(
                                response.getString("authenticatorData")
                            )
                            name("signature").value(
                                response.getString("signature")
                            )
                            name("userHandle").value(
                                response.getString("userHandle")
                            )
                        }
                    }
                ).build()
        )
        val apiResponse = call.await()
        return apiResponse.result("Error calling /signingResponse") {
        }
    }
    
  3. 安全地移除视图中处理意图启动器和活动结果调用的所有方法。

  4. 由于 signInRequest() 现在返回一个 JSONObject,您无需创建 PendingIntent。用 JSONObject 替换返回的 intent,并从您的 API 方法中调用 getCredential()

    suspend fun getPasskey(
        activity: Activity,
        creationResult: JSONObject
    ): GetCredentialResponse? {
        Toast.makeText(
            activity,
            "Fetching previously stored credentials",
            Toast.LENGTH_SHORT
        )
            .show()
        var result: GetCredentialResponse? = null
        try {
            val request = GetCredentialRequest(
                listOf(
                    GetPublicKeyCredentialOption(
                        creationResult.toString(),
                        null
                    ),
                    GetPasswordOption()
                )
            )
            result = credMan.getCredential(activity, request)
            if (result.credential is PublicKeyCredential) {
                val publicKeycredential = result.credential as PublicKeyCredential
                Log.i("TAG", "Passkey ${publicKeycredential.authenticationResponseJson}")
                return result
            }
        } catch (e: Exception) {
            showErrorAlert(activity, e)
        }
        return result
    }
    
  5. 调用成功后,将响应发送回服务器以验证并确认用户身份。此 API 调用的请求和响应参数与 FIDO2 实现类似,因此无需进行任何更改。

其他资源