從 FIDO2 遷移至憑證管理工具

如要在 Android 驗證,建議使用 Credential Manager,這個 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. 以 Credential Manager 初始化取代 FIDO 初始化。請在用於建立密碼金鑰及登入方法的類別中,加入這項宣告:

    val credMan = CredentialManager.create(context)
    

建立密碼金鑰

您需要建立新的密碼金鑰、將金鑰與使用者帳戶建立關聯,並將該金鑰的公開金鑰儲存在伺服器上,使用者才能利用這組金鑰登入。更新註冊函式呼叫後,即可設定應用程式的這項功能。

圖 1. 此圖說明使用憑證管理工具建立密碼金鑰時,系統在應用程式和伺服器之間交換資料的方式。
  1. 如要在建立密碼金鑰時取得傳送至 createCredential() 方法的必要參數,請按照 WebAuthn 規格說明所述,將 name("residentKey").value("required") 新增至您的 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。更新意圖啟動器呼叫,以便從 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 as CreateCredentialRequest,
                    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,並從 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 實作項目,因此不必修改。

其他資源