从 FIDO2 迁移到 Credential Manager

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 服务身份验证。

    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. 将 FIDO 初始化替换为 Credential Manager 初始化。在用于密钥通行证创建和登录方法的类中添加此声明

    val credMan = CredentialManager.create(context)
    

创建密钥通行证

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

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

    suspend fun registerRequest(sessionId: String ... {
        // ...
        .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(
            Request.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. 安全地删除视图中处理意图启动器和活动结果调用的任何方法。

  4. 由于registerRequest()现在返回一个JSONObject,因此您无需创建PendingIntent。将返回的意图替换为JSONObject。更新您的意图启动器调用以从凭据管理器 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 as CreateCredentialRequest,
                    activity
                ) as CreatePublicKeyCredentialResponse
            } catch (e: CreateCredentialException) {
    
                showErrorAlert(activity, e)
    
                return null
            }
            return response
        }
    
  5. 调用成功后,将响应发送回服务器。此调用的请求和响应与 FIDO2 实现类似,因此无需更改。

使用密钥进行身份验证

设置密钥创建后,您可以设置您的应用以允许用户使用他们的密钥登录和身份验证。为此,您需要更新您的身份验证代码以处理凭据管理器结果,并实现一个通过密钥进行身份验证的功能。

图 2. 凭据管理器的密钥身份验证流程。
  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,并从您的 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 实现类似,因此无需更改。

其他资源