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. 做好準備
- 在筆記型電腦上從 credman_codelab 分支 (https://github.com/android/identity-samples/tree/credman_codelab) 複製這個存放區。
- 在 Android Studio 中前往 CredentialManager 模組並開啟專案。
查看應用程式的初始狀態
如要查看應用程式的初始狀態,請按照以下步驟操作:
- 啟動應用程式。
- 你會看到主畫面,當中包含「sign up」和「sign in」按鈕。
- 你可以按一下「sign up」按鈕,使用密碼金鑰或密碼註冊。
- 你可以按一下「sign in」按鈕,使用密碼金鑰和已儲存的密碼登入。
如要瞭解密碼金鑰及其運作方式,請參閱這篇文章。
3. 新增使用密碼金鑰註冊的功能
在採用 Credential Manager API 的 Android 應用程式中註冊新帳戶時,使用者可為帳戶建立密碼金鑰。這個密碼金鑰會安全地儲存在使用者選擇的憑證提供者服務中,且日後會用於登入,讓使用者不必在每次登入時都要輸入密碼。
現在你要使用生物特徵辨識/螢幕鎖定建立密碼金鑰,並註冊使用者憑證。
使用密碼金鑰註冊
依序前往「Credential Manager」->「app」->「main」->「java」->「SignUpFragment.kt」,當中會顯示「username」文字欄位,以及使用密碼金鑰註冊的按鈕。
將驗證問題和其他 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
字典中的重要參數:
參數 | 說明 |
伺服器產生的隨機字串,由於有足夠的熵,因此無法遭到破解。至少應包含 16 個位元組。這是必填欄位,但在註冊期間不會用到 (除非要進行認證)。 | |
使用者的專屬 ID。這個值不得包含個人識別資訊,例如電子郵件地址或使用者名稱。你也可以使用系統為各個帳戶產生的隨機 16 位元組值。 | |
這個欄位應包含使用者能夠認得的帳戶專屬 ID,例如電子郵件地址或使用者名稱。這會顯示在帳戶選取器中 (如要指定使用者名稱,請使用密碼驗證中的值)。 | |
此為選填欄位,用於指定較容易辨識的帳戶名稱。這是使用者能夠理解的使用者帳戶名稱,僅供顯示之用。 | |
Relying Party Entity 會對應到應用程式詳細資料,並且需要以下項目:
| |
Public Key Credential Parameters 是允許的演算法和金鑰類型清單,當中必須包含至少一個元素。 | |
註冊裝置的使用者可能已註冊過其他裝置。如要避免在單一驗證器上為相同帳戶建立多個憑證,你可以忽略這些裝置。transports 成員 (如有提供) 應包含註冊各個憑證時呼叫 getTransports() 的結果。 | |
指定裝置是否應連結至平台 (如果沒有相關要求)。請設為「platform」。這表示我們想將驗證器嵌入平台裝置,且系統不會提示使用者插入 USB 安全金鑰等。 | |
| 指定「required」值來建立密碼金鑰。 |
建立憑證
- 建立 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 偵錯指令查看完整錯誤記錄。
- 最後,你必須將公開金鑰憑證傳送至伺服器並允許使用者登入,藉此完成註冊程序。應用程式會收到包含公開金鑰的憑證物件,你可以將該金鑰傳送至伺服器來註冊密碼金鑰。
在此,我們已使用模擬伺服器,因此要傳回 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
中的重要參數:
參數 | 說明 |
所建立密碼金鑰的 Base64URL 編碼 ID。這個 ID 可在驗證時,協助瀏覽器判斷裝置上是否有相符的密碼金鑰。這個值必須儲存在後端資料庫中。 | |
憑證 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. 新增透過密碼金鑰或密碼驗證的功能
你現在可以使用密碼金鑰或密碼,安全地進行應用程式驗證了。
取得驗證問題和其他選項以傳遞至 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
字典中的重要參數:
參數 | 說明 |
| |
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
物件中的重要參數:
參數 | 說明 |
經驗證密碼金鑰憑證的 Base64URL 編碼 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
標記。