API FIDO2 trên Android đầu tiên của bạn

1. Giới thiệu

API FIDO2 là gì?

FIDO2 API cho phép các ứng dụng Android tạo và dùng thông tin xác thực mạnh mẽ dựa trên khoá công khai đã được chứng thực nhằm mục đích xác thực người dùng. API này cung cấp phương thức triển khai Ứng dụng WebAuthn, hỗ trợ việc sử dụng trình xác thực (khoá bảo mật) chuyển vùng BLE, NFC và USB cũng như trình xác thực nền tảng, cho phép người dùng xác thực bằng vân tay hoặc phương thức khoá màn hình.

Sản phẩm bạn sẽ tạo ra...

Trong lớp học lập trình này, bạn sẽ xây dựng một ứng dụng Android có chức năng xác thực lại đơn giản bằng cảm biến vân tay. "Xác thực lại" là khi người dùng đăng nhập vào một ứng dụng, sau đó xác thực lại khi họ chuyển về ứng dụng của bạn hoặc khi cố gắng truy cập vào một phần quan trọng trong ứng dụng. Trường hợp sau còn được gọi là "xác thực bước".

Kiến thức bạn sẽ học được...

Bạn sẽ tìm hiểu cách gọi API FIDO2 của Android và các tuỳ chọn mà bạn có thể cung cấp để phục vụ nhiều trường hợp. Bạn cũng sẽ tìm hiểu các phương pháp hay nhất dành riêng cho việc xác thực lại.

Những thứ bạn cần...

  • Thiết bị Android có cảm biến vân tay (ngay cả khi không có cảm biến vân tay, phương thức khoá màn hình vẫn có thể cung cấp chức năng xác minh người dùng tương đương)
  • Hệ điều hành Android 7.0 trở lên với các bản cập nhật mới nhất. Hãy nhớ đăng ký vân tay (hoặc phương thức khoá màn hình).

2. Thiết lập

Sao chép Kho lưu trữ

Xem kho lưu trữ GitHub.

https://github.com/android/codelab-fido2

$ git clone https://github.com/android/codelab-fido2.git

Chúng tôi sẽ triển khai những gì?

  • Cho phép người dùng đăng ký "trình xác thực nền tảng xác minh người dùng" (điện thoại Android có cảm biến vân tay sẽ hoạt động như một).
  • Cho phép người dùng xác thực lại bản thân trong ứng dụng bằng vân tay của họ.

Bạn có thể xem trước những gì mình sẽ tạo tại đây.

Bắt đầu dự án lớp học lập trình của bạn

Ứng dụng sau khi hoàn tất sẽ gửi yêu cầu đến một máy chủ tại https://webauthn-codelab.glitch.me. Bạn có thể thử phiên bản web của ứng dụng đó ở đó.

c2234c42ba8a6ef1.png

Bạn sẽ làm việc trên phiên bản ứng dụng của riêng mình.

  1. Truy cập vào trang chỉnh sửa của trang web tại https://glitch.com/edit/#!/webauthn-codelab.
  2. Tìm video "Phối lại để chỉnh sửa" ở góc trên cùng bên phải. Khi nhấn nút này, bạn có thể "nhảy nhánh" mã và tiếp tục với phiên bản của riêng bạn cùng với URL dự án mới. 9ef108869885e4ce.pngS
  3. Sao chép tên dự án ở trên cùng bên trái (bạn có thể sửa đổi tên dự án nếu muốn). c91d0d59c61021a4.png
  4. Dán phần tử đó vào phần HOSTNAME của tệp .env đang gặp sự cố. 889b55b1cf74b894.png.

3. Liên kết ứng dụng và một trang web bằng Digital Asset Links (Đường liên kết đến tài sản kỹ thuật số)

Để sử dụng API FIDO2 trên một ứng dụng Android, hãy liên kết API đó với một trang web và chia sẻ thông tin đăng nhập giữa các ứng dụng đó. Để làm điều đó, hãy tận dụng Đường liên kết đến tài sản kỹ thuật số. Bạn có thể khai báo mối liên kết bằng cách lưu trữ tệp JSON chứa Digital Asset Links (Đường liên kết đến tài sản kỹ thuật số) trên trang web của mình và thêm một đường liên kết đến tệp Digital Asset Links (Đường liên kết đến tài sản kỹ thuật số) vào tệp kê khai của ứng dụng.

Lưu trữ .well-known/assetlinks.json tại miền của bạn

Bạn có thể xác định mối liên kết giữa ứng dụng và trang web bằng cách tạo một tệp JSON rồi đặt tệp đó vào .well-known/assetlinks.json. May mắn thay, chúng ta đã có một mã máy chủ tự động hiển thị tệp assetlinks.json, chỉ bằng cách thêm các tham số môi trường sau vào tệp .env đang gặp sự cố:

  • ANDROID_PACKAGENAME: Tên gói của ứng dụng (com.example.android.fido2)
  • ANDROID_SHA256HASH: Hàm băm SHA256 của chứng chỉ ký

Để lấy hàm băm SHA256 của chứng chỉ ký dành cho nhà phát triển, hãy sử dụng lệnh bên dưới. Mật khẩu mặc định của kho khoá gỡ lỗi là "android".

$ keytool -exportcert -list -v -alias androiddebugkey -keystore ~/.android/debug.keystore

Khi truy cập vào https://<your-project-name>.glitch.me/.well-known/assetlinks.json , bạn sẽ thấy một chuỗi JSON như sau:

[{
  "relation": ["delegate_permission/common.handle_all_urls", "delegate_permission/common.get_login_creds"],
  "target": {
    "namespace": "web",
    "site": "https://<your-project-name>.glitch.me"
  }
}, {
  "relation": ["delegate_permission/common.handle_all_urls", "delegate_permission/common.get_login_creds"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example.android.fido2",
    "sha256_cert_fingerprints": ["DE:AD:BE:EF:..."]
  }
}]

Mở dự án trong Android Studio

Nhấp vào "Open an existing Android Studio project" (Mở một dự án Android Studio hiện có) trên màn hình chào mừng của Android Studio.

Chọn "android" bên trong kiểm tra kho lưu trữ.

1062875cf11ffb95.png.

Liên kết ứng dụng với bản phối lại của bạn

Mở tệp gradle.properties. Ở cuối tệp, hãy thay đổi URL máy chủ thành bản phối lại nhiễu mà bạn vừa tạo.

// ...

# The URL of the server
host=https://<your-project-name>.glitch.me

Đến đây, bạn đã thiết lập xong cấu hình Digital Asset Links (Đường liên kết đến tài sản kỹ thuật số).

4. Xem cách hoạt động hiện tại của ứng dụng

Hãy bắt đầu bằng cách tìm hiểu cách hoạt động của ứng dụng ngay bây giờ. Hãy nhớ chọn "app-start" (bắt đầu ứng dụng) trong hộp kết hợp cấu hình chạy. Nhấp vào "Run" (Chạy) (hình tam giác màu xanh lục bên cạnh hộp kết hợp) để chạy ứng dụng trên thiết bị Android đã kết nối của bạn.

29351fb97062b43c.png.

Khi khởi chạy ứng dụng, bạn sẽ thấy màn hình để nhập tên người dùng của mình. Đây là UsernameFragment. Để minh hoạ, ứng dụng và máy chủ sẽ chấp nhận mọi tên người dùng. Bạn chỉ cần nhập nội dung nào đó rồi nhấn "Tiếp theo".

bd9007614a9a3644.png

Màn hình tiếp theo mà bạn thấy là AuthFragment. Đây là nơi người dùng có thể đăng nhập bằng mật khẩu. Sau đó, chúng tôi sẽ thêm một tính năng để đăng nhập bằng FIDO2 tại đây. Xin nhắc lại rằng để minh hoạ, ứng dụng và máy chủ sẽ chấp nhận mọi mật khẩu. Bạn chỉ cần nhập nội dung rồi nhấn "Đăng nhập".

d9caba817a0a99bd.png

Đây là màn hình cuối cùng của ứng dụng này, HomeFragment. Hiện tại, bạn chỉ thấy một danh sách trống thông tin đăng nhập tại đây. Nhấn "Xác thực lại" sẽ đưa bạn trở lại AuthFragment. Nhấn vào "Đăng xuất" sẽ đưa bạn trở lại UsernameFragment. Nút hành động nổi có dấu "+" hiện không thực hiện bất kỳ hành động nào, nhưng nó sẽ bắt đầu đăng ký

thông tin đăng nhập mới sau khi bạn đã triển khai quy trình đăng ký FIDO2.

1cfcc6c884020e37.png.

Trước khi bắt đầu lập trình, sau đây là một kỹ thuật hữu ích. Trên Android Studio, hãy nhấn vào "TODO" ở dưới cùng. Nó sẽ hiển thị danh sách tất cả VIỆC CẦN LÀM trong lớp học lập trình này. Chúng ta sẽ bắt đầu với TODO đầu tiên trong phần tiếp theo.

e5a811bbc7cd7b30.png

5. Đăng ký thông tin xác thực bằng vân tay

Để bật tính năng xác thực bằng vân tay, trước tiên bạn cần đăng ký thông tin xác thực do trình xác thực nền tảng xác minh người dùng tạo. Đây là một trình xác thực được nhúng trên thiết bị, giúp xác minh người dùng đó bằng dữ liệu sinh trắc học, chẳng hạn như cảm biến vân tay.

37ce78fdf2759832.pngS

Như chúng ta đã thấy trong phần trước, nút hành động nổi hiện không thực hiện thao tác nào. Hãy xem cách đăng ký chứng chỉ mới.

Gọi API máy chủ: /auth/registerRequest

Mở AuthRepository.kt rồi tìm TODO(1).

Ở đây, registerRequest là phương thức được gọi khi nhấn phím FAB. Chúng ta muốn thực hiện phương thức này gọi API máy chủ /auth/registerRequest. API trả về một ApiResult có tất cả PublicKeyCredentialCreationOptions mà ứng dụng cần để tạo thông tin xác thực mới.

Sau đó, chúng ta có thể gọi getRegisterPendingIntent với các tuỳ chọn. API FIDO2 này trả về một PendingIntent trên Android để mở hộp thoại vân tay số và tạo thông tin đăng nhập mới. Sau đó, chúng ta có thể trả về PendingIntent đó cho phương thức gọi.

Sau đó, phương thức này sẽ có dạng như dưới đây.

suspend fun registerRequest(): PendingIntent? {
  fido2ApiClient?.let { client ->
    try {
      val sessionId = dataStore.read(SESSION_ID)!!
      when (val apiResult = api.registerRequest(sessionId)) {
        ApiResult.SignedOutFromServer -> forceSignOut()
        is ApiResult.Success -> {
          if (apiResult.sessionId != null) {
            dataStore.edit { prefs ->
              prefs[SESSION_ID] = apiResult.sessionId
            }
          }
          val task = client.getRegisterPendingIntent(apiResult.data)
          return task.await()
        }
      }
    } catch (e: Exception) {
      Log.e(TAG, "Cannot call registerRequest", e)
    }
  }
  return null
}

Mở hộp thoại vân tay để đăng ký

Mở HomeFragment.kt rồi tìm TODO(2).

Đây là nơi giao diện người dùng nhận lại Ý định từ AuthRepository. Ở đây, chúng ta sẽ sử dụng thành phần createCredentialIntentLauncher để chạy PendingIntent mà chúng ta nhận được ở bước trước. Một hộp thoại để tạo thông tin xác thực sẽ mở ra.

binding.add.setOnClickListener {
  lifecycleScope.launch {
    val intent = viewModel.registerRequest()
    if (intent != null) {
      createCredentialIntentLauncher.launch(
        IntentSenderRequest.Builder(intent).build()
      )
    }
  }
}

Nhận ActivityResult với Thông tin xác thực mới

Mở HomeFragment.kt và tìm TODO(3).

Phương thức handleCreateCredentialResult này được gọi sau khi hộp thoại vân tay đóng. Nếu một thông tin xác thực được tạo thành công, thành phần data của ActivityResult sẽ chứa thông tin xác thực.

Trước tiên, chúng ta phải trích xuất một PublicKeyCredential từ data. Ý định dữ liệu có thêm một trường gồm các mảng byte có khoá Fido.FIDO2_KEY_CREDENTIAL_EXTRA. Bạn có thể sử dụng một phương thức tĩnh trong PublicKeyCredential có tên là deserializeFromBytes để biến mảng byte thành đối tượng PublicKeyCredential.

Tiếp theo, hãy kiểm tra xem thành phần response của đối tượng thông tin xác thực này có phải là AuthenticationErrorResponse hay không. Nếu có, thì đã xảy ra lỗi khi tạo thông tin đăng nhập; nếu không, chúng ta có thể gửi thông tin xác thực đến phần phụ trợ.

Phương thức hoàn chỉnh sẽ có dạng như sau:

private fun handleCreateCredentialResult(activityResult: ActivityResult) {
  val bytes = activityResult.data?.getByteArrayExtra(Fido.FIDO2_KEY_CREDENTIAL_EXTRA)
  when {
    activityResult.resultCode != Activity.RESULT_OK ->
      Toast.makeText(requireContext(), R.string.cancelled, Toast.LENGTH_LONG).show()
    bytes == null ->
      Toast.makeText(requireContext(), R.string.credential_error, Toast.LENGTH_LONG)
        .show()
    else -> {
      val credential = PublicKeyCredential.deserializeFromBytes(bytes)
      val response = credential.response
      if (response is AuthenticatorErrorResponse) {
        Toast.makeText(requireContext(), response.errorMessage, Toast.LENGTH_LONG)
          .show()
      } else {
        viewModel.registerResponse(credential)
      }
    }
  }
}

Gọi API máy chủ: /auth/registerResponse

Mở AuthRepository.kt rồi tìm TODO(4).

Phương thức registerResponse này được gọi sau khi giao diện người dùng tạo thành công thông tin đăng nhập mới và chúng ta muốn gửi lại thông tin này cho máy chủ.

Đối tượng PublicKeyCredential có thông tin về thông tin đăng nhập mới được tạo bên trong. Bây giờ, chúng ta muốn ghi nhớ mã nhận dạng của khoá cục bộ để có thể phân biệt với các khoá khác đã đăng ký trên máy chủ. Trong đối tượng PublicKeyCredential, hãy lấy thuộc tính rawId của đối tượng đó rồi đưa vào biến chuỗi cục bộ bằng toBase64.

Bây giờ, chúng ta đã sẵn sàng gửi thông tin đến máy chủ. Dùng api.registerResponse để gọi API máy chủ và gửi lại phản hồi. Giá trị trả về chứa danh sách tất cả thông tin xác thực đã đăng ký trên máy chủ, bao gồm cả thông tin đăng nhập mới.

Cuối cùng, chúng ta có thể lưu kết quả trong DataStore. Bạn phải lưu danh sách thông tin đăng nhập bằng khoá CREDENTIALS dưới dạng StringSet. Bạn có thể sử dụng toStringSet để chuyển đổi danh sách thông tin xác thực thành StringSet.

Ngoài ra, chúng ta lưu mã thông tin xác thực bằng khoá LOCAL_CREDENTIAL_ID.

suspend fun registerResponse(credential: PublicKeyCredential) {
  try {
    val sessionId = dataStore.read(SESSION_ID)!!
    val credentialId = credential.rawId.toBase64()
    when (val result = api.registerResponse(sessionId, credential)) {
      ApiResult.SignedOutFromServer -> forceSignOut()
      is ApiResult.Success -> {
        dataStore.edit { prefs ->
          result.sessionId?.let { prefs[SESSION_ID] = it }
          prefs[CREDENTIALS] = result.data.toStringSet()
          prefs[LOCAL_CREDENTIAL_ID] = credentialId
        }
      }
    }
  } catch (e: ApiException) {
    Log.e(TAG, "Cannot call registerResponse", e)
  }
}

Chạy ứng dụng để bạn có thể nhấp vào nút hành động nổi rồi đăng ký thông tin xác thực mới.

7d64d9289c5a3cbc.pngS

6. Xác thực người dùng bằng vân tay

Chúng ta hiện đã đăng ký thông tin xác thực trên ứng dụng và máy chủ. Bây giờ, chúng ta có thể sử dụng địa chỉ email này để cho phép người dùng đăng nhập. Chúng tôi đang thêm tính năng đăng nhập bằng vân tay vào AuthFragment. Khi người dùng truy cập vào đó, màn hình sẽ hiện hộp thoại vân tay. Khi xác thực thành công, người dùng sẽ được chuyển hướng đến HomeFragment.

Gọi API máy chủ: /auth/signinRequest

Mở AuthRepository.kt rồi tìm TODO(5).

Phương thức signinRequest này được gọi khi AuthFragment được mở. Ở đây, chúng ta muốn yêu cầu máy chủ và xem liệu chúng ta có thể cho phép người dùng đăng nhập bằng FIDO2 hay không.

Trước tiên, chúng ta phải truy xuất PublicKeyCredentialRequestOptions từ máy chủ. Dùng api.signInRequest để gọi API máy chủ. ApiResult được trả về chứa PublicKeyCredentialRequestOptions.

Với PublicKeyCredentialRequestOptions, chúng ta có thể dùng FIDO2 API getSignIntent nhằm tạo một PendingIntent sau để mở hộp thoại vân tay số.

Cuối cùng, chúng ta có thể đưa PendingIntent trở lại giao diện người dùng.

suspend fun signinRequest(): PendingIntent? {
  fido2ApiClient?.let { client ->
    val sessionId = dataStore.read(SESSION_ID)!!
    val credentialId = dataStore.read(LOCAL_CREDENTIAL_ID)
    if (credentialId != null) {
      when (val apiResult = api.signinRequest(sessionId, credentialId)) {
        ApiResult.SignedOutFromServer -> forceSignOut()
        is ApiResult.Success -> {
          val task = client.getSignPendingIntent(apiResult.data)
          return task.await()
        }
      }
    }
  }
  return null
}

Mở hộp thoại vân tay để xác nhận

Mở AuthFragment.kt rồi tìm TODO(6).

Việc này khá giống với những gì chúng tôi đã làm để đăng ký. Chúng ta có thể mở hộp thoại vân tay với thành viên signIntentLauncher.

viewLifecycleOwner.lifecycleScope.launchWhenStarted {
  launch {
    viewModel.signinRequests.collect { intent ->
      signIntentLauncher.launch(
        IntentSenderRequest.Builder(intent).build()
      )
    }
  }
  launch {
    ...
  }
}

Xử lý ActivityResult

Mở AuthFragment.kt và tìm TODO(7).

Xin nhắc lại, việc này cũng giống như những gì chúng ta đã làm khi đăng ký. Chúng ta có thể trích xuất PublicKeyCredential, kiểm tra lỗi và truyền vào ViewModel.

private fun handleSignResult(activityResult: ActivityResult) {
  val bytes = activityResult.data?.getByteArrayExtra(Fido.FIDO2_KEY_CREDENTIAL_EXTRA)
  when {
    activityResult.resultCode != Activity.RESULT_OK ->
      Toast.makeText(requireContext(), R.string.cancelled, Toast.LENGTH_SHORT).show()
    bytes == null ->
      Toast.makeText(requireContext(), R.string.auth_error, Toast.LENGTH_SHORT)
        .show()
    else -> {
      val credential = PublicKeyCredential.deserializeFromBytes(bytes)
      val response = credential.response
      if (response is AuthenticatorErrorResponse) {
        Toast.makeText(requireContext(), response.errorMessage, Toast.LENGTH_SHORT)
          .show()
      } else {
        viewModel.signinResponse(credential)
      }
    }
  }
}

Gọi API máy chủ: /auth/signinResponse

Mở AuthRepository.kt rồi tìm TODO(8).

Đối tượng PublicKeyCredential có một mã thông tin xác thực trong đó là keyHandle. Giống như trong quy trình đăng ký, hãy lưu biến này trong biến chuỗi cục bộ để có thể lưu trữ sau.

Giờ đây, chúng ta đã sẵn sàng gọi API máy chủ bằng api.signinResponse. Giá trị trả về chứa danh sách thông tin xác thực.

Lúc này, quá trình đăng nhập đã thành công. Chúng ta phải lưu trữ tất cả kết quả trong DataStore. Danh sách thông tin xác thực phải được lưu trữ dưới dạng StringSet bằng khoá CREDENTIALS. Mã thông tin xác thực cục bộ mà chúng ta đã lưu ở trên phải được lưu trữ dưới dạng chuỗi có khoá LOCAL_CREDENTIAL_ID.

Cuối cùng, chúng ta cần cập nhật trạng thái đăng nhập để giao diện người dùng có thể chuyển hướng người dùng đến HomeFragment. Bạn có thể thực hiện việc này bằng cách phát một đối tượng SignInState.SignedIn đến SharedFlow có tên là signInStateMutable. Chúng ta cũng muốn gọi refreshCredentials để tìm nạp thông tin xác thực của người dùng để thông tin này được liệt kê trong giao diện người dùng.

suspend fun signinResponse(credential: PublicKeyCredential) {
  try {
    val username = dataStore.read(USERNAME)!!
    val sessionId = dataStore.read(SESSION_ID)!!
    val credentialId = credential.rawId.toBase64()
    when (val result = api.signinResponse(sessionId, credential)) {
      ApiResult.SignedOutFromServer -> forceSignOut()
      is ApiResult.Success -> {
        dataStore.edit { prefs ->
          result.sessionId?.let { prefs[SESSION_ID] = it }
          prefs[CREDENTIALS] = result.data.toStringSet()
          prefs[LOCAL_CREDENTIAL_ID] = credentialId
        }
        signInStateMutable.emit(SignInState.SignedIn(username))
        refreshCredentials()
      }
    }
  } catch (e: ApiException) {
    Log.e(TAG, "Cannot call registerResponse", e)
  }
}

Chạy ứng dụng rồi nhấp vào "Xác thực lại" để mở AuthFragment. Lúc này, bạn sẽ thấy một hộp thoại vân tay nhắc bạn đăng nhập bằng vân tay.

45f81419f84952c8.pngS

Xin chúc mừng! Giờ đây, bạn đã tìm hiểu cách sử dụng API FIDO2 trên Android để đăng ký và đăng nhập.

7. Xin chúc mừng!

Bạn đã hoàn tất thành công lớp học lập trình – API FIDO2 dành cho Android đầu tiên của bạn.

Kiến thức bạn học được

  • Cách đăng ký thông tin xác thực bằng trình xác thực nền tảng xác minh người dùng.
  • Cách xác thực người dùng bằng trình xác thực đã đăng ký.
  • Các tuỳ chọn hiện có để đăng ký trình xác thực mới.
  • Các phương pháp hay nhất về trải nghiệm người dùng để xác thực lại bằng cảm biến sinh trắc học.

Bước tiếp theo

  • Tìm hiểu cách tạo ra trải nghiệm tương tự trong trang web.

Bạn có thể tìm hiểu bằng cách tham gia lớp học lập trình WebAuthn đầu tiên!

Tài nguyên

Xin đặc biệt cảm ơn Yuriy Ackerman của Liên minh FIDO vì đã giúp đỡ bạn.