您的第一個 Android FIDO2 API

1. 簡介

什麼是 FIDO2 API?

FIDO2 API 可讓 Android 應用程式建立及使用經過認證的高強度公開金鑰憑證,藉此驗證使用者。API 提供 WebAuthn 用戶端實作項目,支援使用 BLE、NFC 和 USB 漫遊驗證器 (安全金鑰) 和平台驗證器,讓使用者可使用指紋或螢幕鎖定功能進行驗證。

建構內容...

在本程式碼研究室中,您將使用指紋感應器建構具有簡易重新驗證功能的 Android 應用程式。「重新驗證」當使用者登入應用程式時,或嘗試存取應用程式中的重要部分時,就會進行重新驗證。後者也稱為「進階驗證」。

課程內容:

您將瞭解如何呼叫 Android FIDO2 API,以及您可以提供的選項,藉此因應各種情況。你也會學到重新驗證相關最佳做法。

軟硬體需求

  • 搭載指紋感應器的 Android 裝置 (即使沒有指紋感應器,螢幕鎖定功能也能提供同等的使用者驗證功能)
  • 搭載最新更新的 Android 作業系統 7.0 以上版本。請務必註冊指紋 (或螢幕鎖定)。

2. 開始設定

複製存放區

查看 GitHub 存放區。

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

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

要導入哪些設定?

  • 允許使用者註冊「使用者驗證平台驗證器」(搭載指紋感應器的 Android 手機本身皆可)。
  • 允許使用者透過指紋重新驗證應用程式。

您可以在這裡預覽要建構的內容。

開始程式碼研究室專案

完成的應用程式將要求傳送至位於 https://webauthn-codelab.glitch.me 的伺服器。你可以在該裝置上試用同一應用程式的網頁版。

c2234c42ba8a6ef1.png

而您將使用自己的應用程式版本。

  1. 前往網站的編輯頁面:https://glitch.com/edit/#!/webauthn-codelab
  2. 找到「重混」。按下按鈕即可「分支」並使用新的專案網址搭配您自己的版本。9ef108869885e4ce.png
  3. 複製左上方的專案名稱 (您可以視需要修改)。c91d0d59c61021a4.png
  4. 將程式碼貼到 .env 檔案的 HOSTNAME 區段 (出現故障)。889b55b1cf74b894.png

3. 使用 Digital Asset Links 為應用程式和網站建立關聯

如要在 Android 應用程式中使用 FIDO2 API,請將 API 與網站建立關聯,並在各應用程式之間共用憑證。方法很簡單,只要使用 Digital Asset Links 就行了。如要宣告關聯,您可以在網站上代管 Digital Asset Links JSON 檔案,然後在應用程式資訊清單中加入 Digital Asset Link 檔案的連結。

代管您網域的 .well-known/assetlinks.json

您可以建立 JSON 檔案並放入 .well-known/assetlinks.json,藉此定義應用程式和網站之間的關聯。幸好,我們擁有一個伺服器程式碼,可自動顯示 assetlinks.json 檔案,只要將下列環境變數新增至 .env 檔案 glitch 中:

  • ANDROID_PACKAGENAME:應用程式的套件名稱 (com.example.android.fido2)
  • ANDROID_SHA256HASH:簽署憑證的 SHA256 雜湊

如要取得開發人員簽署憑證的 SHA256 雜湊,請使用下列指令。偵錯 KeyStore 的預設密碼為「android」。

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

存取 https://<your-project-name>.glitch.me/.well-known/assetlinks.json 後,您應該會看到類似下方的 JSON 字串:

[{
  "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:..."]
  }
}]

在 Android Studio 中開啟專案

按一下「Open an existing Android Studio project」開啟 App Engine 應用程式

選擇「Android」查看存放區

1062875cf11ffb95.png

將應用程式與重混作品建立關聯

開啟 gradle.properties 檔案將主機網址變更為您剛建立的 Glitch 重混版本。

// ...

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

這時 Digital Asset Links 設定應已備妥。

4. 瞭解應用程式目前的運作方式

首先來看看應用程式的運作方式。請務必在執行設定下拉式方塊中選取「app-start」。按一下「執行」(下拉式方塊旁的綠色三角形) 可在已連結的 Android 裝置上啟動應用程式。

29351fb97062b43c.png

啟動應用程式時,畫面會顯示輸入使用者名稱的畫面。目前播放的是 UsernameFragment。為了示範,應用程式和伺服器可接受任何使用者名稱。只要輸入文字,再按 [下一步] 即可。

bd9007614a9a3644.png

下一個畫面是 AuthFragment。這就是使用者可以使用密碼登入的位置。我們之後會新增功能來在這裡使用 FIDO2 登入。同樣地,為了示範,應用程式和伺服器接受任何密碼。只要輸入文字並按下「登入」即可。

d9caba817a0a99bd.png

這是這個應用程式的最後一個畫面:HomeFragment。目前這裡只會顯示空白的憑證清單。正在按下「重新驗證」即可返回「AuthFragment」。按下「登出」即可返回「UsernameFragment」。含有「+」的懸浮動作按鈕目前不會執行任何動作,但會啟動

1cfcc6c884020e37.png

在開始寫程式之前,不妨先參考以下實用技巧。在 Android Studio 上,按下「TODO」。會列出本程式碼研究室中所有 TODO 的清單。我們將從下一節的第一項 TODO 開始。

e5a811bbc7cd7b30.png

5. 使用指紋註冊憑證

如要使用指紋進行驗證,您必須先註冊一個由驗證平台驗證器的使用者產生的憑證。憑證是一種裝置內嵌的驗證器,可透過生物特徵辨識 (例如指紋感應器) 驗證使用者。

37ce78fdf2759832.png

如上一節所述,懸浮動作按鈕目前沒有任何作用。讓我們來看看如何註冊新的憑證。

呼叫伺服器 API:/auth/registerRequest

開啟 AuthRepository.kt 並找到 TODO(1)。

此處 registerRequest 是在按下懸浮動作按鈕 (FAB) 時呼叫的方法。我們想讓這個方法呼叫伺服器 API /auth/registerRequest。API 會傳回 ApiResult,其中包含用戶端產生新憑證時需要的所有 PublicKeyCredentialCreationOptions

接著,我們可以使用選項呼叫 getRegisterPendingIntent。這個 FIDO2 API 會傳回 Android PendingIntent,以便開啟指紋對話方塊並產生新憑證,我們可以將該 PendingIntent 傳回呼叫端。

此方法將如下所示。

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
}

開啟指紋對話方塊進行註冊

開啟 HomeFragment.kt 並找到 TODO(2)。

這就是 UI 從 AuthRepository 取得意圖的位置。在這裡,我們會使用 createCredentialIntentLauncher 成員啟動先前步驟之後取得的 PendingIntent。系統隨即會開啟產生憑證的對話方塊。

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

使用新憑證接收 ActivityResult

開啟 HomeFragment.kt 並找出 TODO(3)。

指紋對話方塊關閉後,系統就會呼叫這個 handleCreateCredentialResult 方法。如果成功產生憑證,ActivityResult 的 data 成員會包含憑證資訊。

首先,我們必須從 data 擷取公開金鑰憑證。資料意圖中有一個額外的位元組陣列欄位,欄位為 Fido.FIDO2_KEY_CREDENTIAL_EXTRA。您可以在 PublicKeyCredential 中使用名為 deserializeFromBytes 的靜態方法,將位元組陣列轉換為 PublicKeyCredential 物件。

接著,檢查這個憑證物件的 response 成員是否為 AuthenticationErrorResponse。如果是,就表示產生憑證時發生錯誤;否則可以將憑證傳送至後端

已完成的方法如下所示:

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)
      }
    }
  }
}

呼叫伺服器 API:/auth/registerResponse

開啟 AuthRepository.kt 並找到 TODO(4)。

當 UI 成功產生新憑證後,我們就會將憑證傳回伺服器,然後呼叫此 registerResponse 方法。

PublicKeyCredential 物件會包含新產生的憑證相關資訊。現在我們要記住本機金鑰的 ID,以便與在伺服器上註冊的其他金鑰有所區別。在 PublicKeyCredential 物件中,取得其 rawId 屬性,並使用 toBase64 將其放入本機字串變數中。

我們現在準備將資訊傳送至伺服器。使用 api.registerResponse 呼叫伺服器 API 並傳回回應。傳回的值包含伺服器上註冊的所有憑證清單,包括新的憑證。

最後,我們可以將結果儲存在 DataStore 中。憑證清單應以 CREDENTIALS 鍵做為 StringSet 儲存。您可以使用 toStringSet 將憑證清單轉換為 StringSet

此外,我們也會使用金鑰 LOCAL_CREDENTIAL_ID 儲存憑證 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)
  }
}

執行應用程式,您就能點選懸浮動作按鈕 (FAB) 並註冊新憑證。

7d64d9289c5a3cbc.png

6. 使用指紋驗證使用者

現在我們已在應用程式和伺服器上註冊憑證。現在可以用於讓使用者登入。我們正在為「AuthFragment」新增指紋登入功能。使用者造訪後,系統會顯示指紋對話方塊。驗證成功後,系統會將使用者重新導向至 HomeFragment

呼叫伺服器 API:/auth/signinRequest

開啟 AuthRepository.kt 並找到 TODO(5)。

開啟 AuthFragment 時,系統會呼叫這個 signinRequest 方法。在此,我們要要求伺服器,並查看是否能讓使用者以 FIDO2 登入。

首先,我們必須從伺服器擷取 PublicKeyCredentialRequestOptions。使用 api.signInRequest 呼叫伺服器 API。傳回的 ApiResult 包含 PublicKeyCredentialRequestOptions

透過 PublicKeyCredentialRequestOptions,我們可以使用 FIDO2 API getSignIntent 建立 PendingIntent 來開啟指紋對話方塊。

最後,我們可以將 PendingIntent 傳回使用者介面。

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
}

開啟指紋對話方塊以進行斷言

開啟 AuthFragment.kt 並找到 TODO(6)。

這和我們在註冊時的做法大致相同。我們可以使用 signIntentLauncher 成員啟動指紋對話方塊。

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

處理 ActivityResult

開啟 AuthFragment.kt 並找到 TODO(7)。

再次提醒,這和我們在註冊時的做法相同。我們可以擷取 PublicKeyCredential、檢查錯誤,並將其傳遞至 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)
      }
    }
  }
}

呼叫伺服器 API:/auth/signinResponse

開啟 AuthRepository.kt 並找到 TODO(8)。

PublicKeyCredential 物件內含的憑證 ID,格式為 keyHandle。如同註冊流程中的做法,我把這個 ID 儲存為本機字串變數,方便日後儲存。

我們已經準備好使用 api.signinResponse 呼叫伺服器 API。傳回的值包含憑證清單。

此時已成功登入。我們必須將所有結果儲存在 DataStore 中。憑證清單應以 CREDENTIALS 鍵的 StringSet 形式儲存。上方儲存的本機憑證 ID 應以字串 LOCAL_CREDENTIAL_ID 的形式儲存。

最後,我們必須更新登入狀態,讓 UI 將使用者重新導向至 HomeFragment。方法是將 SignInState.SignedIn 物件發出至名為 signInStateMutable 的 SharedFlow。我們也想要呼叫 refreshCredentials 來擷取使用者憑證,以便列入使用者介面中。

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)
  }
}

執行應用程式並按一下「重新驗證」開啟「AuthFragment」。畫面上應會顯示指紋對話方塊,提示使用指紋登入帳戶。

45f81419f84952c8.png

恭喜!您已瞭解如何在 Android 上使用 FIDO2 API 註冊及登入。

7. 恭喜!

您已成功完成程式碼研究室:「您的第一個 Android FIDO2 API」

目前所學內容

  • 如何透過使用者驗證平台驗證器來註冊憑證。
  • 如何使用已註冊的驗證器來驗證使用者。
  • 註冊新驗證器的可用選項。
  • 使用生物特徵辨識感應器重新驗證的使用者體驗最佳做法。

下一步

  • 瞭解如何在網站上打造類似體驗。

您可以透過您的第一個 WebAuthn 程式碼研究室瞭解詳情!

資源

在此特別感謝 FIDO 聯盟的 Yuriy Ackermann 提供相關協助。