從 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() {     // ...     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. 從檢視畫面中,安全地移除處理意圖啟動器和活動結果呼叫的方法。

  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 = 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,並從 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 實作項目,因此不必修改。

其他資源