Sinh trắc học không thay thế đăng nhập với server — nó chỉ mở khoá một thứ đã lưu sẵn trên máy (thường là refresh token) hoặc xác nhận một thao tác (chuyển tiền).
Cách yếu: gọi prompt, nhận true/false rồi cho vào app. Kết quả chỉ là một boolean trên client — app bị patch hoặc hook là bỏ qua được. local_auth (Flutter) hay nhiều thư viện RN mặc định hoạt động theo kiểu này.
Cách đúng trên Android: gắn sinh trắc học với khoá trong Keystore qua CryptoObject. Khoá được tạo với setUserAuthenticationRequired(true), nên chỉ giải mã được khi người dùng vừa xác thực thành công:
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Unlock your account")
.setAllowedAuthenticators(BIOMETRIC_STRONG)
.setNegativeButtonText("Use password")
.build()
// Cipher initialised with a Keystore key that requires user authentication
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
// onAuthenticationSucceeded: result.cryptoObject.cipher decrypts the stored refresh tokenCác điểm cần nhắc:
- Kiểm tra trước bằng BiometricManager.canAuthenticate(...); chưa đăng ký vân tay thì hướng dẫn hoặc dùng mật khẩu.
- BIOMETRIC_STRONG (Class 3) mới dùng được với CryptoObject; BIOMETRIC_WEAK chỉ hợp để xác nhận nhẹ.
- setInvalidatedByBiometricEnrollment(true): người dùng thêm vân tay mới → khoá bị huỷ → bắt đăng nhập lại bằng mật khẩu (chống việc người khác thêm vân tay của họ).
- iOS: lưu token trong Keychain với access control yêu cầu biometry.
Lưu ý: luôn có đường dự phòng (mật khẩu/PIN) và thao tác nhạy cảm vẫn phải được server xác thực lại.