1. Trước khi bắt đầu
Các giải pháp xác thực truyền thống đặt ra một số thách thức về bảo mật và khả năng hữu dụng.
Mật khẩu được sử dụng rộng rãi nhưng...
- Dễ quên
- Người dùng cần có kiến thức để tạo được mật khẩu mạnh.
- Dễ bị lừa đảo, thu thập và bị kẻ tấn công dùng trộm qua các cuộc tấn công phát lại (replay attack).
Android đã nỗ lực tạo ra Credential Manager API nhằm đơn giản hoá trải nghiệm đăng nhập và giải quyết các rủi ro về bảo mật bằng cách hỗ trợ khoá truy cập – tiêu chuẩn ngành thế hệ mới nhằm xác thực mà không cần mật khẩu.
API này kết hợp tính năng hỗ trợ khoá truy cập với các phương thức xác thực truyền thống như mật khẩu, Đăng nhập bằng Google, v.v.
Người dùng sẽ có thể tạo khoá truy cập cũng như lưu trữ khoá truy cập trong Trình quản lý mật khẩu của Google (dịch vụ này sẽ đồng bộ hoá các khoá truy cập trên những thiết bị Android mà người dùng đăng nhập). Khoá truy cập phải được tạo, liên kết với tài khoản người dùng và có khoá công khai tương ứng được lưu trữ trên một máy chủ trước khi người dùng có thể dùng để đăng nhập.
Trong lớp học lập trình này, bạn sẽ tìm hiểu cách dùng Credential Manager API để đăng ký bằng khoá truy cập và mật khẩu, rồi sau này sử dụng chúng cho mục đích xác thực. Có 2 quy trình như sau:
- Đăng ký: bằng khoá truy cập và mật khẩu.
- Đăng nhập: bằng khoá truy cập và mật khẩu.
Điều kiện tiên quyết
- Có kiến thức cơ bản về cách chạy ứng dụng trong Android Studio.
- Có kiến thức cơ bản về quy trình xác thực trong ứng dụng Android.
- Có kiến thức cơ bản về khoá truy cập.
Kiến thức bạn sẽ học được
- Cách tạo khoá truy cập.
- Cách lưu mật khẩu trong trình quản lý mật khẩu.
- Cách xác thực người dùng bằng khoá truy cập hoặc mật khẩu đã lưu.
Bạn cần có
Một trong những tổ hợp thiết bị sau:
- Một thiết bị Android chạy Android 9 trở lên (đối với khoá truy cập) và Android 4.4 trở lên (đối với quy trình xác thực mật khẩu thông qua Credential Manager API).
- Tốt nhất là thiết bị có cảm biến sinh trắc học.
- Hãy nhớ đăng ký một dữ liệu sinh trắc học (hoặc phương thức khoá màn hình)
- Trình bổ trợ Kotlin phiên bản 1.8.10
2. Bắt đầu thiết lập
- Sao chép kho lưu trữ này trên máy tính của bạn từ nhánh credman_codelab: https://github.com/android/identity-samples/tree/credman_codelab
- Đến mô-đun CredentialManager rồi mở dự án này trong Android Studio.
Hãy xem trạng thái ban đầu của ứng dụng
Để xem cách hoạt động của trạng thái ban đầu của ứng dụng, hãy làm theo các bước sau:
- Khởi chạy ứng dụng.
- Bạn sẽ thấy một màn hình chính có một nút đăng ký và đăng nhập.
- Bạn có thể nhấp vào nút đăng ký để đăng ký bằng một khoá truy cập hoặc mật khẩu.
- Bạn có thể nhấp vào nút đăng nhập để đăng nhập bằng khoá truy cập hoặc mật khẩu đã lưu.
Để nắm được khái niệm cũng như cách hoạt động của khoá truy cập, hãy xem bài viết Khoá truy cập hoạt động như thế nào? .
3. Thêm tính năng đăng nhập bằng khoá truy cập
Khi đăng ký tài khoản mới trên một ứng dụng Android sử dụng Credential Manager API, người dùng có thể tạo một khoá truy cập cho tài khoản của mình. Khoá truy cập này sẽ được lưu trữ an toàn bên phía trình cung cấp thông tin đăng nhập mà người dùng chọn và sẽ được dùng để đăng nhập trong các lần sau mà không đòi hỏi người dùng phải nhập mật khẩu mỗi lần đăng nhập.
Giờ thì bạn sẽ tạo một khoá truy cập và đăng ký thông tin đăng nhập của người dùng bằng dữ liệu sinh trắc học/khoá màn hình.
Đăng ký bằng khoá truy cập
Bên trong Credential Manager -> app -> main -> java -> SignUpFragment.kt, bạn có thể thấy một trường văn bản "username" và một nút để đăng ký bằng khoá truy cập.
Truyền thử thách (challenge) này và phản hồi JSON khác vào lệnh gọi createPasskey()
Trước khi tạo khoá truy cập, bạn cần yêu cầu máy chủ lấy thông tin cần thiết sẽ được truyền vào Credential Manager API trong quá trình gọi createCredential().
May mắn là bạn đã có một phản hồi mô phỏng trong tài sản của mình (RegFromServer.txt), phản hồi này sẽ trả về những tham số như vậy trong lớp học lập trình này.
- Trong ứng dụng của bạn, hãy di chuyển đến SignUpFragment.kt, tìm phương thức signUpWithPasskeys tại nơi bạn dự định viết logic tạo khoá truy cập và cho phép người dùng đăng nhập. Bạn có thể tìm thấy phương thức này trong chính lớp đó.
- Kiểm tra khối else có dòng nhận xét để gọi createPasskey() và thay thế bằng đoạn mã sau:
SignUpFragment.kt
//TODO : Call createPasskey() to signup with passkey
val data = createPasskey()
Phương thức này sẽ được gọi khi bạn điền một tên người dùng hợp lệ trên màn hình.
- Bên trong phương thức createPasskey(), bạn cần tạo một CreatePublicKeyCredentialRequest() với các tham số cần thiết được trả về.
SignUpFragment.kt
//TODO create a CreatePublicKeyCredentialRequest() with necessary registration json from server
val request = CreatePublicKeyCredentialRequest(fetchRegistrationJsonFromServer())
fetchRegistrationJsonFromServer() này là một phương thức đọc phản hồi JSON đăng ký từ tài sản và trả về JSON đăng ký sẽ được truyền trong quá trình tạo khoá truy cập.
- Hãy tìm phương thức fetchRegistrationJsonFromServer() rồi thay thế phần TODO bằng đoạn mã sau để trả về JSON cũng như xoá câu lệnh trả về chuỗi trống:
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())
- Tại đây, bạn sẽ đọc JSON đăng ký từ tài sản.
- Dữ liệu JSON này có 4 trường cần thay thế.
- UserId (mã nhận dạng người dùng) cần phải là duy nhất để người dùng có thể tạo nhiều khoá truy cập (nếu cần). Bạn sẽ thay <userId> bằng userId được tạo.
- <challenge> cũng cần phải là duy nhất để bạn có thể tạo ra một thử thách ngẫu nhiên duy nhất. Phương thức này đã có trong đoạn mã của bạn.
Đoạn mã sau đây chứa các lựa chọn mẫu mà bạn sẽ nhận được từ máy chủ:
{
"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"
}
}
Bảng sau đây không đầy đủ nhưng có chứa các tham số quan trọng trong từ điển PublicKeyCredentialCreationOptions
:
Tham số | Mô tả |
Một chuỗi ngẫu nhiên do máy chủ tạo ra đủ ngẫu nhiên đến mức không đoán ra được. Độ dài tối thiểu là 16 byte. Chuỗi này là bắt buộc nhưng sẽ không được dùng trong quá trình đăng ký, trừ phi để thực hiện việc chứng thực. | |
Mã nhận dạng duy nhất của người dùng. Giá trị này không được chứa thông tin nhận dạng cá nhân, chẳng hạn như địa chỉ email hoặc tên người dùng. Có thể là một giá trị 16 byte ngẫu nhiên được tạo cho mỗi tài khoản. | |
Trường này phải chứa một mã nhận dạng duy nhất mà người dùng sẽ nhận ra đối với tài khoản của mình, như địa chỉ email hoặc tên người dùng. Thông tin này sẽ xuất hiện trong bộ chọn tài khoản. (Nếu dùng tên người dùng, hãy sử dụng chính giá trị như khi xác thực mật khẩu.) | |
Trường này là một tên tài khoản không bắt buộc và thân thiện với người dùng hơn. Đó là một cái tên dễ hiểu đối với con người, được đặt cho tài khoản người dùng và chỉ nhằm mục đích hiển thị. | |
Thực thể Bên tin cậy (Relying Party – RP) tương ứng với thông tin cụ thể của ứng dụng. Thực thể này cần có:
| |
Tham số thông tin đăng nhập khoá công khai (Public Key Credential Parameters) là danh sách thuật toán và loại khoá được phép. Danh sách này phải chứa ít nhất một phần tử. | |
Khi đang tìm cách đăng ký một thiết bị, có thể người dùng từng đăng ký các thiết bị khác rồi. Để hạn chế việc tạo nhiều thông tin đăng nhập cho cùng một tài khoản trên một trình xác thực duy nhất, bạn có thể bỏ qua các thiết bị này. Thành phần transports (nếu được cung cấp) sẽ chứa kết quả của lệnh gọi getTransports() trong quá trình đăng ký từng thông tin đăng nhập. | |
cho biết liệu thiết bị có gắn liền với nền tảng hay không, hoặc cho biết nếu không có yêu cầu nào về điều đó. Hãy thiết lập thành phần này thành "platform" (nền tảng). Điều này cho thấy rằng chúng ta muốn một trình xác thực được nhúng vào thiết bị nền tảng, và người dùng sẽ không được nhắc về việc gắn khoá bảo mật (ví dụ: USB). | |
| chỉ ra một giá trị "bắt buộc" để tạo khoá truy cập. |
Tạo thông tin đăng nhập
- Sau khi tạo một CreatePublicKeyCredentialRequest(), bạn sẽ cần gọi phương thức gọi createCredential() bằng yêu cầu đã tạo.
SignUpFragment.kt
//TODO call createCredential() with createPublicKeyCredentialRequest
try {
response = credentialManager.createCredential(
requireActivity(),
request
) as CreatePublicKeyCredentialResponse
} catch (e: CreateCredentialException) {
configureProgress(View.INVISIBLE)
handlePasskeyFailure(e)
}
- Bạn truyền thông tin cần thiết vào createCredential().
- Sau khi yêu cầu này thành công, bạn sẽ thấy một bảng dưới cùng màn hình nhắc bạn tạo khoá truy cập.
- Giờ đây người dùng có thể xác minh danh tính của mình thông qua dữ liệu sinh trắc học hoặc phương thức khoá màn hình, v.v.
- Bạn xử lý chế độ hiển thị của thành phần hiển thị được kết xuất, đồng thời xử lý các ngoại lệ nếu yêu cầu gặp lỗi hoặc không thành công vì lý do nào đó. Tại đây, các thông báo lỗi sẽ được ghi vào nhật ký và hiện trong một hộp thoại lỗi trên ứng dụng. Bạn có thể kiểm tra nhật ký đầy đủ về các lỗi thông qua Android Studio hoặc lệnh gỡ lỗi adb
- Bây giờ, sau cùng thì bạn cần hoàn tất quy trình đăng ký bằng cách gửi thông tin đăng nhập khoá công khai đến máy chủ và cho phép người dùng đăng nhập. Ứng dụng sẽ nhận được một đối tượng thông tin đăng nhập (credential) chứa khoá công khai mà bạn có thể gửi đến máy chủ để đăng ký khoá truy cập.
Ở đây, chúng ta đã sử dụng một máy chủ mô phỏng, vì vậy chúng ta chỉ cần trả về true để cho biết rằng máy chủ đã lưu khoá công khai đã đăng ký cho mục đích xác minh và xác thực sau này.
Trong phương thức signUpWithPasskeys(), hãy tìm dòng nhận xét có liên quan và thay bằng đoạn mã sau:
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 trả về giá trị true, cho biết rằng máy chủ (mô phỏng) đã lưu khoá công khai đó để sử dụng sau này.
- Bạn thiết lập cờ SignedInThroughPasskeys thành true, cho biết rằng bạn đang đăng nhập qua khoá truy cập.
- Sau khi đăng nhập, bạn chuyển hướng người dùng đến màn hình chính.
Đoạn mã sau đây có chứa các lựa chọn ví dụ mà bạn sẽ nhận được:
{
"id": String,
"rawId": String,
"type": "public-key",
"response": {
"clientDataJSON": String,
"attestationObject": String,
}
}
Bảng sau đây không đầy đủ nhưng có chứa các tham số quan trọng trong PublicKeyCredential
:
Tham số | Mô tả |
Mã nhận dạng được mã hoá Base64URL của khoá truy cập đã tạo. Trong quá trình xác thực thì mã nhận dạng này sẽ giúp trình duyệt xác định xem thiết bị có khoá truy cập phù hợp hay không. Giá trị này phải được lưu trữ trong cơ sở dữ liệu ở máy chủ phụ trợ. | |
Một phiên bản đối tượng | |
Một đối tượng | |
Một đối tượng chứng thực mã hoá bằng |
Chạy ứng dụng và bạn sẽ nhấp được vào nút Sign up with passkeys (Đăng ký bằng khoá truy cập) để tạo khoá truy cập.
4. Lưu mật khẩu trong Trình cung cấp thông tin đăng nhập (Credential Provider)
Trong ứng dụng này, trong màn hình SignUp (Đăng ký), bạn đã đăng ký bằng tên người dùng và mật khẩu được triển khai cho mục đích minh hoạ.
Để lưu thông tin mật khẩu đăng nhập của người dùng với trình cung cấp mật khẩu của họ, bạn sẽ triển khai một CreatePasswordRequest để truyền vào createCredential() nhằm lưu mật khẩu này.
- Tìm phương thức signUpWithPassword(), thay phần TODO thành phương thức gọi createPassword:
SignUpFragment.kt
//TODO : Save the user credential password with their password provider
createPassword()
- Trong phương thức createPassword(), bạn cần tạo yêu cầu mật khẩu như sau. Hãy thay thế phần TODO bằng đoạn mã sau:
SignUpFragment.kt
//TODO : CreatePasswordRequest with entered username and password
val request = CreatePasswordRequest(
binding.username.text.toString(),
binding.password.text.toString()
)
- Tiếp theo, trong phương thức createPassword(), bạn cần tạo thông tin đăng nhập bằng yêu cầu tạo mật khẩu và lưu thông tin mật khẩu đăng nhập của người dùng với trình cung cấp mật khẩu của họ. Hãy thay phần TODO bằng đoạn mã sau:
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)
}
- Giờ thì bạn đã lưu thành công thông tin mật khẩu đăng nhập với trình cung cấp mật khẩu của người dùng để xác thực qua mật khẩu chỉ bằng một thao tác nhấn.
5. Thêm tính năng xác thực bằng khoá truy cập hoặc mật khẩu
Giờ thì bạn đã sẵn sàng sử dụng tính năng này như một cách để xác thực ứng dụng của mình sao cho an toàn.
Nhận thử thách và các lựa chọn khác để truyền vào phương thức gọi getPasskey()
Trước khi yêu cầu người dùng xác thực, bạn cần yêu cầu các tham số cần truyền vào JSON WebAuthn từ máy chủ, trong đó có một thử thách (challenge).
Bạn đã có một phản hồi mô phỏng trong tài sản của mình (AuthFromServer.txt), phản hồi này sẽ trả về những tham số như vậy trong lớp học lập trình này.
- Trong ứng dụng của bạn, hãy di chuyển đến SignInFragment.kt rồi tìm phương thức
signInWithSavedCredentials
– trong đó bạn sẽ viết logic để xác thực thông qua mật khẩu hoặc khoá truy cập đã lưu và cho phép người dùng đăng nhập: - Kiểm tra khối else có dòng nhận xét để gọi createPasskey() và thay thế bằng đoạn mã sau:
SignInFragment.kt
//TODO : Call getSavedCredentials() method to signin using passkey/password
val data = getSavedCredentials()
- Trong phương thức getSavedCredentials(), bạn cần tạo một
GetPublicKeyCredentialOption
() với các tham số cần thiết để nhận được thông tin đăng nhập từ trình cung cấp thông tin đăng nhập của bạn.
SigninFragment.kt
//TODO create a GetPublicKeyCredentialOption() with necessary registration json from server
val getPublicKeyCredentialOption =
GetPublicKeyCredentialOption(fetchAuthJsonFromServer(), null)
fetchAuthJsonFromServer() là một phương thức đọc phản hồi JSON xác thực từ tài sản và trả về JSON xác thực để truy xuất tất cả khoá truy cập được liên kết với tài khoản người dùng này.
Tham số thứ 2: clientDataHash – một hàm băm được sử dụng để xác minh danh tính bên tin cậy, chỉ thiết lập hàm này nếu bạn đã thiết lập GetCredentialRequest.origin. Đối với ứng dụng mẫu, tham số này rỗng.
Tham số thứ 3 sẽ là true nếu bạn muốn tác vụ đang thực hiện quay lại ngay lập tức khi không có thông tin đăng nhập thay vì quay lại khám phá thông tin đăng nhập từ xa, và là false (mặc định) nếu bạn muốn điều ngược lại.
- Tìm phương thức fetchAuthJsonFromServer() rồi thay phần TODO và thay bằng đoạn mã sau để trả về JSON cũng như xoá câu lệnh trả về chuỗi trống:
SignInFragment.kt
//TODO fetch authentication mock json
return requireContext().readFromAsset("AuthFromServer")
Lưu ý: Máy chủ của lớp học lập trình này được thiết kế để trả về một JSON giống nhất có thể với từ điển PublicKeyCredentialRequestOptions
được truyền tới phương thức gọi getCredential() của API. Đoạn mã sau có chứa một vài lựa chọn ví dụ mà bạn sẽ nhận được:
{
"challenge": String,
"rpId": String,
"userVerification": "",
"timeout": 1800000
}
Bảng sau đây không đầy đủ nhưng có chứa các tham số quan trọng trong từ điển PublicKeyCredentialRequestOptions
:
Tham số | Mô tả |
Một thử thách do máy chủ tạo trong một đối tượng | |
Mã nhận dạng RP là một miền. Một trang web có thể chỉ định miền của nó hoặc chỉ định một hậu tố có thể đăng ký. Giá trị này phải khớp với tham số |
- Tiếp theo, bạn cần tạo một đối tượngPasswordOption() để truy xuất tất cả mật khẩu đã lưu của tài khoản người dùng này trong trình cung cấp mật khẩu thông qua Credential Manager API. Trong phương thức getSavedCredentials(), hãy tìm phần TODO rồi thay bằng đoạn mã sau:
SigninFragment.kt
//TODO create a PasswordOption to retrieve all the associated user's password
val getPasswordOption = GetPasswordOption()
Lấy thông tin đăng nhập
- Tiếp theo, bạn sẽ cần gọi yêu cầu getCredential() với tất cả lựa chọn trên để truy xuất thông tin đăng nhập được liên kết:
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.
}
- Bạn truyền thông tin cần thiết tới getCredential(). Thao tác này sẽ lấy danh sách lựa chọn thông tin đăng nhập và bối cảnh hoạt động để cho thấy các lựa chọn ở bảng dưới cùng trong bối cảnh đó.
- Sau khi yêu cầu thành công, bạn sẽ thấy một bảng ở dưới cùng màn hình liệt kê tất cả thông tin đăng nhập đã tạo đối với tài khoản được liên kết.
- Giờ đây, người dùng có thể xác minh danh tính của mình thông qua dữ liệu sinh trắc học hoặc phương thức khoá màn hình, v.v. nhằm xác thực thông tin đăng nhập đã chọn.
- Bạn thiết lập cờ SignedInThroughPasskeys thành true, cho biết rằng bạn đang đăng nhập qua khoá truy cập. Còn không thì cờ này sẽ là false.
- Bạn xử lý chế độ hiển thị của thành phần hiển thị được kết xuất, đồng thời xử lý các ngoại lệ nếu yêu cầu gặp lỗi hoặc không thành công vì lý do nào đó. Tại đây, các thông báo lỗi sẽ được ghi vào nhật ký và hiện trong một hộp thoại lỗi trên ứng dụng. Bạn có thể kiểm tra nhật ký đầy đủ về các lỗi thông qua Android Studio hoặc lệnh gỡ lỗi adb
- Bây giờ, sau cùng thì bạn cần hoàn tất quy trình đăng ký bằng cách gửi thông tin đăng nhập khoá công khai đến máy chủ và cho phép người dùng đăng nhập. Ứng dụng sẽ nhận được một đối tượng thông tin đăng nhập chứa khoá công khai mà bạn có thể gửi đến máy chủ để xác thực thông qua khoá truy cập.
Ở đây, chúng ta đã sử dụng một máy chủ mô phỏng, vì vậy chúng ta chỉ cần trả về true để cho biết rằng máy chủ đã xác thực khoá công khai đó.
Trong phương thức signInWithSavedCredentials
(), hãy tìm dòng nhận xét có liên quan và thay bằng đoạn mã sau:
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() trả về true, cho biết máy chủ (mô phỏng) đã xác thực khoá công khai để sử dụng sau này.
- Sau khi đăng nhập, bạn chuyển hướng người dùng đến màn hình chính.
Đoạn mã sau đây chứa một đối tượng PublicKeyCredential
mẫu:
{
"id": String
"rawId": String
"type": "public-key",
"response": {
"clientDataJSON": String
"authenticatorData": String
"signature": String
"userHandle": String
}
}
Bảng sau đây không đầy đủ nhưng có chứa các tham số quan trọng trong đối tượng PublicKeyCredential
:
Tham số | Mô tả |
Mã nhận dạng được mã hoá Base64URL của thông tin đăng nhập bằng khoá truy cập đã được xác thực. | |
Một phiên bản đối tượng | |
Một đối tượng | |
Một đối tượng | |
Một đối tượng | |
Một đối tượng |
Chạy ứng dụng, chuyển đến phần Sign in -> Sign in with passkeys/saved password (Đăng nhập -> Đăng nhập bằng khoá truy cập/mật khẩu đã lưu) rồi thử đăng nhập bằng thông tin đăng nhập đã lưu.
Dùng thử
Bạn đã triển khai quy trình tạo khoá truy cập, lưu mật khẩu trong Credential Manager và xác thực thông qua khoá truy cập hoặc mật khẩu đã lưu bằng Credential Manager API trên ứng dụng Android của mình.
6. Xin chúc mừng!
Bạn đã hoàn thành lớp học lập trình này! Nếu bạn muốn kiểm tra giải pháp hoàn chỉnh, hãy xem tại https://github.com/android/identity-samples/tree/main/CredentialManager
Nếu có thắc mắc, bạn hãy đặt câu hỏi trên StackOverflow và gắn thẻ passkey
.