瞭解如何在 Android 應用程式中使用 Credential Manager API 簡化驗證流程

1. 事前準備

傳統驗證解決方案會造成多種安全性與可用性問題。

密碼雖然廣受使用,但有以下缺點

  • 容易忘記
  • 使用者必須知道如何設定高強度密碼
  • 容易遭受網路釣魚、竊取和重送攻擊

Android 致力建立 Credential Manager API 來簡化登入流程,同時支援密碼金鑰,透過這項新一代無密碼驗證業界標準來因應安全性風險。

Credential Manager 支援密碼金鑰,並結合了密碼和「使用 Google 帳戶登入」功能等傳統驗證方式。

使用者將可建立密碼金鑰,並將其儲存在 Google 密碼管理工具中,以便在已登入的所有 Android 裝置上同步密碼金鑰。如要透過密碼金鑰登入,使用者必須建立密碼金鑰並連結至使用者帳戶,再將其公開金鑰儲存在伺服器上。

在本程式碼研究室中,你將瞭解如何透過 Credential Manager API 使用密碼金鑰和密碼登入,並在日後用於進行驗證。你將進行以下 2 個流程:

  • 使用密碼金鑰和密碼註冊
  • 使用密碼金鑰和已儲存的密碼登入

必要條件

  • 對於如何在 Android Studio 中執行應用程式有基本瞭解
  • 對於 Android 應用程式中的驗證流程有基本瞭解
  • 對於密碼金鑰有基本瞭解

課程內容

  • 如何建立密碼金鑰
  • 如何將密碼儲存在密碼管理工具中
  • 如何使用密碼金鑰或已儲存的密碼驗證使用者

課程需求

請準備下列其中一組裝置:

  • 搭載 Android 9 以上版本 (使用密碼金鑰驗證) 和 Android 4.4 以上版本 (透過 Credential Manager API 進行密碼驗證) 的 Android 裝置
  • 裝置最好配備生物特徵辨識感應器
  • 務必要設定生物特徵辨識功能或螢幕鎖定
  • Kotlin 外掛程式版本:1.8.10

2. 做好準備

  1. 在筆記型電腦上從 credman_codelab 分支 (https://github.com/android/identity-samples/tree/credman_codelab) 複製這個存放區。
  2. 在 Android Studio 中前往 CredentialManager 模組並開啟專案。

查看應用程式的初始狀態

如要查看應用程式的初始狀態,請按照以下步驟操作:

  1. 啟動應用程式。
  2. 你會看到主畫面,當中包含「sign up」和「sign in」按鈕。
  3. 你可以按一下「sign up」按鈕,使用密碼金鑰或密碼註冊。
  4. 你可以按一下「sign in」按鈕,使用密碼金鑰和已儲存的密碼登入。

8c0019ff9011950a.jpeg

如要瞭解密碼金鑰及其運作方式,請參閱這篇文章

3. 新增使用密碼金鑰註冊的功能

在採用 Credential Manager API 的 Android 應用程式中註冊新帳戶時,使用者可為帳戶建立密碼金鑰。這個密碼金鑰會安全地儲存在使用者選擇的憑證提供者服務中,且日後會用於登入,讓使用者不必在每次登入時都要輸入密碼。

現在你要使用生物特徵辨識/螢幕鎖定建立密碼金鑰,並註冊使用者憑證。

使用密碼金鑰註冊

依序前往「Credential Manager」->「app」->「main」->「java」->「SignUpFragment.kt」,當中會顯示「username」文字欄位,以及使用密碼金鑰註冊的按鈕。

dcc5c529b310f2fb.jpeg

將驗證問題和其他 JSON 回應傳遞至 createPasskey() 呼叫

在建立密碼金鑰前,你必須要求伺服器在 createCredential() 呼叫期間將必要資訊傳遞至 Credential Manager API。

幸好你的資產中已有模擬回應 (RegFromServer.txt),會在本程式碼研究室中傳回這類參數。

  • 在應用程式中前往 SignUpFragment.kt,找到「signUpWithPasskeys」方法來撰寫相關邏輯,以便建立密碼金鑰並允許使用者登入。你可以在相同類別中找到該方法。
  • 查看包含 createPasskey() 呼叫註解的 else 區塊,並替換成以下程式碼:

SignUpFragment.kt

//TODO : Call createPasskey() to signup with passkey

val data = createPasskey()

如果你在畫面上輸入有效使用者名稱,系統就會呼叫這個方法。

  • 在 createPasskey() 方法中,你必須使用傳回的必要參數建立 CreatePublicKeyCredentialRequest()

SignUpFragment.kt

//TODO create a CreatePublicKeyCredentialRequest() with necessary registration json from server

val request = CreatePublicKeyCredentialRequest(fetchRegistrationJsonFromServer())

這個 fetchRegistrationJsonFromServer() 方法會讀取資產的註冊 JSON 回應,並傳回要在建立密碼金鑰時傳遞的註冊 JSON。

  • 找到 fetchRegistrationJsonFromServer() 方法,並將 TODO 替換成以下程式碼來傳回 JSON,同時移除空白字串回傳敘述:

SignUpFragment.kt

//TODO fetch registration mock response

val response = requireContext().readFromAsset("RegFromServer")

//Update userId,challenge, name and Display name in the mock
return response.replace("<userId>", getEncodedUserId())
   .replace("<userName>", binding.username.text.toString())
   .replace("<userDisplayName>", binding.username.text.toString())
   .replace("<challenge>", getEncodedChallenge())
  • 在此,你要讀取資產的註冊 JSON。
  • 這個 JSON 有 4 個要替換的欄位。
  • 使用者 ID 不得重複,確保使用者能夠視需要建立多個密碼金鑰。請將 <userId> 替換為所產生的使用者 ID。
  • <challenge> 也不得重複,因此你要產生隨機的不重複驗證問題。這個方法已納入程式碼中。

以下程式碼片段包含伺服器提供的範例選項:

{
  "challenge": String,
  "rp": {
    "name": String,
    "id": String
  },
  "user": {
    "id": String,
    "name": String,
    "displayName": String
  },
  "pubKeyCredParams": [
    {
      "type": "public-key",
      "alg": -7
    },
    {
      "type": "public-key",
      "alg": -257
    }
  ],
  "timeout": 1800000,
  "attestation": "none",
  "excludeCredentials": [],
  "authenticatorSelection": {
    "authenticatorAttachment": "platform",
    "requireResidentKey": true,
    "residentKey": "required",
    "userVerification": "required"
  }
}

下表僅列出部分內容,但包含 PublicKeyCredentialCreationOptions 字典中的重要參數:

參數

說明

challenge

伺服器產生的隨機字串,由於有足夠的熵,因此無法遭到破解。至少應包含 16 個位元組。這是必填欄位,但在註冊期間不會用到 (除非要進行認證)。

user.id

使用者的專屬 ID。這個值不得包含個人識別資訊,例如電子郵件地址或使用者名稱。你也可以使用系統為各個帳戶產生的隨機 16 位元組值。

user.name

這個欄位應包含使用者能夠認得的帳戶專屬 ID,例如電子郵件地址或使用者名稱。這會顯示在帳戶選取器中 (如要指定使用者名稱,請使用密碼驗證中的值)。

user.displayName

此為選填欄位,用於指定較容易辨識的帳戶名稱。這是使用者能夠理解的使用者帳戶名稱,僅供顯示之用。

rp.id

Relying Party Entity 會對應到應用程式詳細資料,並且需要以下項目:

  • 名稱 (必填):你的應用程式名稱
  • ID (選填):對應至網域或子網域。如未提供,則會使用目前的網域
  • 圖示 (選填)

pubKeyCredParams

Public Key Credential Parameters 是允許的演算法和金鑰類型清單,當中必須包含至少一個元素。

excludeCredentials

註冊裝置的使用者可能已註冊過其他裝置。如要避免在單一驗證器上為相同帳戶建立多個憑證,你可以忽略這些裝置。transports 成員 (如有提供) 應包含註冊各個憑證時呼叫 getTransports() 的結果。

authenticatorSelection.authenticatorAttachment

指定裝置是否應連結至平台 (如果沒有相關要求)。請設為「platform」。這表示我們想將驗證器嵌入平台裝置,且系統不會提示使用者插入 USB 安全金鑰等。

residentKey

指定「required」值來建立密碼金鑰。

建立憑證

  1. 建立 CreatePublicKeyCredentialRequest() 後,你必須使用所建立的要求呼叫 createCredential() 呼叫。

SignUpFragment.kt

//TODO call createCredential() with createPublicKeyCredentialRequest

try {
   response = credentialManager.createCredential(
       requireActivity(),
       request
   ) as CreatePublicKeyCredentialResponse
} catch (e: CreateCredentialException) {
   configureProgress(View.INVISIBLE)
   handlePasskeyFailure(e)
}
  • 你要將必要資訊傳遞至 createCredential()。
  • 要求成功後,畫面上會顯示底部功能表,提示你建立密碼金鑰。
  • 現在使用者可透過生物特徵辨識或螢幕鎖定等方式驗證身分。
  • 你可控管是否要顯示所產生的檢視畫面,以及要求因故失敗或未成功時的例外狀況。在此,系統會將錯誤訊息記錄下來,並顯示在應用程式的錯誤對話方塊中。你可以透過 Android Studio 或 ADB 偵錯指令查看完整錯誤記錄。

93022cb87c00f1fc.png

  1. 最後,你必須將公開金鑰憑證傳送至伺服器並允許使用者登入,藉此完成註冊程序。應用程式會收到包含公開金鑰的憑證物件,你可以將該金鑰傳送至伺服器來註冊密碼金鑰。

在此,我們已使用模擬伺服器,因此要傳回 true,表示伺服器已儲存註冊的公開金鑰供日後驗證時使用。

在 signUpWithPasskeys() 方法中,找到相關註解並替換成以下程式碼:

SignUpFragment.kt

//TODO : complete the registration process after sending public key credential to your server and let the user in

data?.let {
   registerResponse()
   DataProvider.setSignedInThroughPasskeys(true)
   listener.showHome()
}
  • registerResponse 會傳回 true,表示 (模擬) 伺服器已儲存公開金鑰供日後使用。
  • 將 SignedInThroughPasskeys 旗標設為 true,表示你要透過密碼金鑰登入。
  • 登入後,你要將使用者重新導向至主畫面。

以下程式碼片段包含所提供的範例選項:

{
  "id": String,
  "rawId": String,
  "type": "public-key",
  "response": {
    "clientDataJSON": String,
    "attestationObject": String,
  }
}

下表僅列出部分內容,但包含 PublicKeyCredential 中的重要參數:

參數

說明

id

所建立密碼金鑰的 Base64URL 編碼 ID。這個 ID 可在驗證時,協助瀏覽器判斷裝置上是否有相符的密碼金鑰。這個值必須儲存在後端資料庫中。

rawId

憑證 ID 的 ArrayBuffer 物件版本。

response.clientDataJSON

ArrayBuffer 物件編碼的用戶端資料。

response.attestationObject

ArrayBuffer 編碼的認證物件,當中包含重要資訊,例如 RP ID、旗標和公開金鑰。

執行應用程式,你將可點按「Sign up with passkeys」按鈕並建立密碼金鑰。

4. 將密碼儲存在憑證提供者服務中

為了方便示範,這個應用程式的註冊畫面中已有註冊好的使用者名稱和密碼。

如要透過密碼提供者服務儲存使用者密碼憑證,你必須實作 CreatePasswordRequest 來傳遞至 createCredential(),藉此儲存密碼。

  • 找到 signUpWithPassword() 方法,並取代 TODO 來發出 createPassword 呼叫:

SignUpFragment.kt

//TODO : Save the user credential password with their password provider

createPassword()
  • createPassword() 方法中,你必須建立這類密碼要求,並將 TODO 替換為以下程式碼:

SignUpFragment.kt

//TODO : CreatePasswordRequest with entered username and password

val request = CreatePasswordRequest(
   binding.username.text.toString(),
   binding.password.text.toString()
)
  • 接著在 createPassword() 方法中,你必須透過密碼建立要求建立憑證,並透過使用者的密碼提供者服務儲存密碼憑證,再將 TODO 替換成以下程式碼:

SignUpFragment.kt

//TODO : Create credential with created password request


try {
   credentialManager.createCredential(request, requireActivity()) as CreatePasswordResponse
} catch (e: Exception) {
   Log.e("Auth", " Exception Message : " + e.message)
}
  • 你已成功透過使用者的密碼提供者服務儲存密碼憑證,讓使用者輕觸一下即可透過密碼驗證。

5. 新增透過密碼金鑰或密碼驗證的功能

你現在可以使用密碼金鑰或密碼,安全地進行應用程式驗證了。

629001f4a778d4fb.png

取得驗證問題和其他選項以傳遞至 getPasskey() 呼叫

在要求使用者進行驗證前,你必須要求從伺服器傳遞 WebAuthn JSON 形式的參數,包括驗證問題。

你的資產中已有模擬回應 (AuthFromServer.txt),會在本程式碼研究室中傳回這類參數。

  • 在應用程式中前往 SignInFragment.kt,找到 signInWithSavedCredentials 方法來撰寫相關邏輯,以便透過已儲存的密碼金鑰或密碼進行驗證,並允許使用者登入。
  • 查看包含 createPasskey() 呼叫註解的 else 區塊,並替換成以下程式碼:

SignInFragment.kt

//TODO : Call getSavedCredentials() method to signin using passkey/password

val data = getSavedCredentials()
  • 在 getSavedCredentials() 方法中,你必須建立 GetPublicKeyCredentialOption(),當中包含從憑證提供者服務取得憑證所需的參數。

SigninFragment.kt

//TODO create a GetPublicKeyCredentialOption() with necessary registration json from server

val getPublicKeyCredentialOption =
   GetPublicKeyCredentialOption(fetchAuthJsonFromServer(), null)

這個 fetchAuthJsonFromServer() 方法會讀取資產的驗證 JSON 回應,並傳回驗證 JSON,藉此擷取與這個使用者帳戶相關聯的所有密碼金鑰。

第 2 個參數為 clientDataHash,這個雜湊是用於驗證依賴方身分,只有在你已設定 GetCredentialRequest.origin 時才須設定。在範例應用程式中,這個參數為空值。

如果你想在沒有可用憑證時,讓作業立即傳回,而不改為尋找遠端憑證,則第 3 個參數要設為 true,反之則設為 false (預設值)。

  • 找到 fetchAuthJsonFromServer() 方法,並將 TODO 替換成以下程式碼來傳回 JSON,同時移除空白字串回傳敘述:

SignInFragment.kt

//TODO fetch authentication mock json

return requireContext().readFromAsset("AuthFromServer")

附註:本程式碼研究室的伺服器會盡可能傳回與傳遞至 API getCredential() 呼叫的 PublicKeyCredentialRequestOptions 字典類似的 JSON。以下程式碼片段包含幾個所提供的範例選項:

{
  "challenge": String,
  "rpId": String,
  "userVerification": "",
  "timeout": 1800000
}

下表僅列出部分內容,但包含 PublicKeyCredentialRequestOptions 字典中的重要參數:

參數

說明

challenge

ArrayBuffer 物件中伺服器產生的驗證問題。你必須提供這個值來防範重送攻擊。切勿在單一回應中接受相同驗證問題兩次。可視為 CSRF 權杖

rpId

RP ID 是網域。網站可指定其網域或可註冊的後置字串。這個值必須與建立密碼金鑰時使用的 rp.id 參數一致。

  • 接著你必須建立 PasswordOption() 物件,透過 Credential Manager API 為這個使用者帳戶擷取所有已儲存至密碼提供者服務的密碼。在 getSavedCredentials() 方法中,找到 TODO 並替換成以下內容:

SigninFragment.kt

//TODO create a PasswordOption to retrieve all the associated user's password

val getPasswordOption = GetPasswordOption()

取得憑證

  • 接著你必須使用上述所有選項呼叫 getCredential() 要求,擷取相關憑證:

SignInFragment.kt

//TODO call getCredential() with required credential options

val result = try {
   credentialManager.getCredential(
       requireActivity(),
       GetCredentialRequest(
           listOf(
               getPublicKeyCredentialOption,
               getPasswordOption
           )  
     )
   )
} catch (e: Exception) {
   configureViews(View.INVISIBLE, true)
   Log.e("Auth", "getCredential failed with exception: " + e.message.toString())
   activity?.showErrorAlert(
       "An error occurred while authenticating through saved credentials. Check logs for additional details"
   )
   return null
}

if (result.credential is PublicKeyCredential) {
   val cred = result.credential as PublicKeyCredential
   DataProvider.setSignedInThroughPasskeys(true)
   return "Passkey: ${cred.authenticationResponseJson}"
}
if (result.credential is PasswordCredential) {
   val cred = result.credential as PasswordCredential
   DataProvider.setSignedInThroughPasskeys(false)
   return "Got Password - User:${cred.id} Password: ${cred.password}"
}
if (result.credential is CustomCredential) {
   //If you are also using any external sign-in libraries, parse them here with the utility functions provided.
}

  • 你必須將必要資訊傳遞至 getCredential()。系統會根據憑證選項清單和活動情境,在該情境的底部功能表中顯示適當選項。
  • 要求成功後,畫面上會顯示底部功能表,列出相關聯帳戶的所有已建立憑證。
  • 現在使用者可透過生物特徵辨識或螢幕鎖定等方式驗證身分,進而驗證所選憑證。
  • 將 SignedInThroughPasskeys 旗標設為 true,表示你要透過密碼金鑰登入,反之則設為 false。
  • 你可控管是否要顯示所產生的檢視畫面,以及要求因故失敗或未成功時的例外狀況。在此,系統會將錯誤訊息記錄下來,並顯示在應用程式的錯誤對話方塊中。你可以透過 Android Studio 或 ADB 偵錯指令查看完整錯誤記錄。
  • 最後,你必須將公開金鑰憑證傳送至伺服器並允許使用者登入,藉此完成註冊程序。應用程式會收到包含公開金鑰的憑證物件,你可以將該金鑰傳送至伺服器,以便透過密碼金鑰進行驗證。

在此,我們已使用模擬伺服器,因此要傳回 true,表示伺服器已驗證公開金鑰。

signInWithSavedCredentials() 方法中,找到相關註解並替換成以下程式碼:

SignInFragment.kt

//TODO : complete the authentication process after validating the public key credential to your server and let the user in.

data?.let {
   sendSignInResponseToServer()
   listener.showHome()
}
  • sendSigninResponseToServer() 會傳回 true,表示 (模擬) 伺服器已驗證公開金鑰以供日後使用。
  • 登入後,你要將使用者重新導向至主畫面。

以下程式碼片段包含範例 PublicKeyCredential 物件:

{
  "id": String
  "rawId": String
  "type": "public-key",
  "response": {
    "clientDataJSON": String
    "authenticatorData": String
    "signature": String
    "userHandle": String
  }
}

下表僅列出部分內容,但包含 PublicKeyCredential 物件中的重要參數:

參數

說明

id

經驗證密碼金鑰憑證的 Base64URL 編碼 ID。

rawId

憑證 ID 的 ArrayBuffer 物件版本。

response.clientDataJSON

用戶端資料的 ArrayBuffer 物件。這個欄位包含多項資訊,例如驗證問題和 RP 伺服器必須驗證的來源。

response.authenticatorData

驗證器資料的 ArrayBuffer 物件。這個欄位包含 RP ID 等資訊。

response.signature

簽名的 ArrayBuffer 物件。這個值是驗證作業的核心元素,必須在伺服器上進行驗證。

response.userHandle

ArrayBuffer 物件,當中包含建立時設定的使用者 ID。如果伺服器必須選擇要使用的 ID 值,或是後端想避免建立憑證 ID 索引,則可使用這個值而非憑證 ID。

執行應用程式,依序選取「sign in」->「Sign in with passkeys/saved password」,然後試著使用已儲存的憑證登入。

立即試試

你已在 Android 應用程式中實作密碼金鑰建立機制,將密碼儲存在 Credential Manager 中,並使用 Credential Manager API 透過密碼金鑰或已儲存的密碼進行驗證。

6. 恭喜!

你已完成本程式碼研究室!如要查看最終解決方案,請前往 https://github.com/android/identity-samples/tree/main/CredentialManager

如有任何疑問,請前往 StackOverflow 提問並加上 passkey 標記

瞭解詳情