初めての Android FIDO2 API

1. はじめに

FIDO2 API とは何ですか?

FIDO2 API を使用すると、Android アプリはユーザーの認証を目的として、実証済みの強力な公開鍵ベースの認証情報を作成して使用できます。この API は WebAuthn Client の実装を提供します。WebAuthn Client は BLE、NFC、USB ローミング認証システム(セキュリティ キー)と、ユーザーが指紋認証または画面ロックを使用して認証できるプラットフォーム認証システムをサポートしています。

作成するアプリの概要

この Codelab では、指紋認証センサーを使用したシンプルな再認証機能を備えた Android アプリを作成します。「再認証」これは、ユーザーがアプリにログインしてからアプリに戻ったときや、アプリの重要なセクションにアクセスしようとしたときに再認証を受けることです。後者の場合は「ステップアップ認証」とも呼ばれます。

学習内容

Android FIDO2 API を呼び出す方法と、さまざまな状況に対応するために利用できるオプションについて学習します。また、再認証固有のベスト プラクティスについても説明します。

必要なもの

  • 指紋認証センサーを搭載した Android デバイス(指紋認証センサーがない場合でも、画面ロックで同等のユーザー認証機能を提供可能)
  • 最新のアップデートがインストールされた Android OS 7.0 以降。指紋(または画面ロック)を登録します。

2. 設定方法

リポジトリのクローンを作成する

GitHub リポジトリを確認する。

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

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

何を実装するか?

  • ユーザーが「ユーザー検証プラットフォーム認証システム」を登録できるようにする(指紋認証センサーを備えた Android スマートフォン自体が指紋認証センサーとして機能します)。
  • ユーザーが指紋を使用してアプリに対して自身を再認証できるようにします。

何を作成されるかについては、こちらからプレビューできます。

Codelab プロジェクトを開始する

完成したアプリが https://webauthn-codelab.glitch.me にあるサーバーにリクエストを送信します。そちらで、同じアプリのウェブ版をお試しください。

c2234c42ba8a6ef1.png

ここでは、独自のバージョンのアプリを使用します。

  1. ウェブサイトの編集ページ(https://glitch.com/edit/#!/webauthn-codelab)に移動します。
  2. [リミックスして編集] を見つけます。ボタンをタップします。ボタンを押すと「フォーク」できますし、新しいプロジェクト URL を指定して独自のバージョンを続行します。9ef108869885e4ce.png
  3. 左上のプロジェクト名をコピーします(必要に応じて変更できます)。c91d0d59c61021a4.png
  4. グリッチで .env ファイルの HOSTNAME セクションに貼り付けます。889b55b1cf74b894.png

3. デジタル アセット リンクでアプリとウェブサイトを関連付ける

Android アプリで FIDO2 API を使用するには、FIDO2 API をウェブサイトと関連付けて、ウェブサイト間で認証情報を共有します。これを行うには、デジタル アセット リンクを利用します。関連付けを宣言するには、ウェブサイトでデジタル アセット リンクの JSON ファイルをホストし、デジタル アセット リンク ファイルへのリンクをアプリのマニフェストに追加します。

ドメインで .well-known/assetlinks.json をホストする

アプリとウェブサイトの関連付けを定義するには、JSON ファイルを作成して .well-known/assetlinks.json に配置します。幸いなことに、不具合のある .env ファイルに次の環境パラメータを追加するだけで、assetlinks.json ファイルを自動的に表示するサーバーコードがあります。

  • ANDROID_PACKAGENAME: アプリのパッケージ名(com.example.android.fido2)
  • ANDROID_SHA256HASH: 署名証明書の SHA256 ハッシュ

デベロッパーの署名証明書の SHA256 ハッシュを取得するには、次のコマンドを使用します。デバッグ キーストアのデフォルトのパスワードは「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] をクリックします。Android Studio のウェルカム画面に表示されます。

[android] を選択します。チェックアウトします。

1062875cf11ffb95.png

アプリをリミックスに関連付ける

gradle.properties ファイルを開きます。ファイルの下部にあるホスト URL を、先ほど作成した Glitch リミックスに変更します。

// ...

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

これで、デジタル アセット リンクの設定は完了です。

4. アプリの現在の動作を見る

まず、アプリの動作を確認してみましょう。実行構成コンボボックスで必ず [app-start] を選択してください。[実行] をクリックします。(コンボボックスの横にある緑色の三角形のアイコン)をクリックして、接続された Android デバイスでアプリを起動します。

29351fb97062b43c.png

アプリを起動すると、ユーザー名を入力する画面が表示されます。これは UsernameFragment です。このデモでは、アプリとサーバーは任意のユーザー名を受け入れます。何かを入力して [次へ] を押してください。

bd9007614a9a3644.png

次に表示される画面は AuthFragment です。ユーザーはパスワードを使用してログインできます。後ほど、ここに FIDO2 でログインする機能を追加します。ここでも、デモの目的から、アプリとサーバーはあらゆるパスワードを受け入れます。何かを入力して [ログイン] を押すだけです。

d9caba817a0a99bd.png

これはこのアプリの最後の画面、HomeFragment です。今のところ、認証情報の空のリストだけがここに表示されます。[Reauth] を押すAuthFragment に戻ります。[ログアウト] を押すUsernameFragment に戻ります。「+」が付いたフローティング アクション ボタンこの段階では何も行われませんが、

新しい認証情報が必要です。

1cfcc6c884020e37.png

コーディングを始める前に、ここで役立つ手法を紹介します。Android Studio で TODO を押します。] をクリックします。この Codelab のすべての TODO のリストが表示されます。次のセクションでは、最初の TODO から始めます。

e5a811bbc7cd7b30.png

5. フィンガープリントを使用して認証情報を登録する

指紋を使用した認証を有効にするには、まず、プラットフォーム認証システム(指紋認証センサーなどの生体認証システムを使用してユーザーを確認する)を確認する、ユーザーが生成した認証情報を登録する必要があります。

37ce78fdf2759832.png

前のセクションで見たように、フローティング アクション ボタンでは何も行われません。新しい認証情報を登録する方法を見ていきましょう。

サーバー API /auth/registerRequest を呼び出します。

AuthRepository.kt を開き、TODO(1) を見つけます。

ここで、registerRequest は FAB が押されたときに呼び出されるメソッドです。このメソッドでサーバー API /auth/registerRequest を呼び出すようにします。API は、クライアントが新しい認証情報を生成するために必要なすべての PublicKeyCredentialCreationOptions を含む ApiResult を返します。

次に、オプションで 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()
      )
    }
  }
}

新しい Credential で ActivityResult を受け取る

HomeFragment.kt を開き、TODO(3) を見つけます。

この handleCreateCredentialResult メソッドは、指紋ダイアログが閉じた後に呼び出されます。認証情報が正常に生成された場合は、ActivityResult の data メンバーに認証情報が含まれます。

まず、data から PublicKeyCredential を抽出する必要があります。データ インテントには、キー Fido.FIDO2_KEY_CREDENTIAL_EXTRA を持つバイト配列の追加フィールドがあります。PublicKeyCredentialdeserializeFromBytes という静的メソッドを使用して、バイト配列を 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) を見つけます。

この registerResponse メソッドは、UI で新しい認証情報が正常に生成された後、それをサーバーに送り返すために呼び出されます。

PublicKeyCredential オブジェクトには、内部に新しく生成された認証情報に関する情報が含まれています。ここで、ローカルキーの ID を覚えておき、サーバーに登録されている他のキーと区別できるようにします。PublicKeyCredential オブジェクトで、rawId プロパティを取得し、toBase64 を使用してローカル文字列変数に設定します。

これで、情報をサーバーに送信する準備が整いました。api.registerResponse を使用してサーバー API を呼び出し、レスポンスを返します。戻り値には、サーバーに登録されているすべての認証情報(新しい認証情報を含む)のリストが含まれます。

これで、結果を DataStore に保存できます。認証情報のリストは、キー CREDENTIALSStringSet として保存する必要があります。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) を見つけます。

この signinRequest メソッドは、AuthFragment が開かれたときに呼び出されます。ここでは、サーバーにリクエストして、ユーザーが FIDO2 でログインできるかどうかを確認します。

まず、サーバーから PublicKeyCredentialRequestOptions を取得する必要があります。api.signInRequest を使用してサーバー API を呼び出します。返される ApiResult には PublicKeyCredentialRequestOptions が含まれます。

PublicKeyCredentialRequestOptions では、FIDO2 API の getSignIntent を使用して、指紋認証ダイアログを開く PendingIntent を作成できます。

これで、PendingIntent を UI に返すことができます。

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 オブジェクトには、keyHandle として認証情報 ID が含まれています。登録フローで行ったように、後で格納できるようにこれをローカルの文字列変数に保存しましょう。

これで、api.signinResponse を使用してサーバー API を呼び出す準備が整いました。戻り値には、認証情報のリストが含まれます。

この時点でログインに成功しています。すべての結果を DataStore に保存する必要があります。認証情報のリストは、キー CREDENTIALS を持つ StringSet として保存する必要があります。上記で保存したローカル認証情報 ID は、キー LOCAL_CREDENTIAL_ID を持つ文字列として保存する必要があります。

最後に、ログイン状態を更新して、UI がユーザーを HomeFragment にリダイレクトできるようにする必要があります。これを行うには、SignInState.SignedIn オブジェクトを signInStateMutable という名前の SharedFlow に出力します。また、refreshCredentials を呼び出してユーザーの認証情報を取得し、UI に一覧表示できるようにします。

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

アプリを実行し、[Reauth] をクリックします。AuthFragment を開きます。指紋のダイアログで、指紋によるログインを求められます。

45f81419f84952c8.png

おめでとうございます。Android で FIDO2 API を使用して登録とログインを行う方法を学習しました。

7. 完了

Codelab - 初めての Android FIDO2 API が完了しました。

学習した内容

  • プラットフォーム認証システムを確認するユーザーを使用して認証情報を登録する方法。
  • 登録済みの認証システムを使用してユーザーを認証する方法。
  • 新しい認証システムを登録する際に利用できるオプション。
  • 生体認証センサーを使用した再認証の UX に関するおすすめの方法。

次のステップ

  • 同様のエクスペリエンスをウェブサイトで構築する方法を学びます。

詳しくは、初めての WebAuthn の Codelab をご覧ください。

リソース

FIDO Alliance の Yuriy Ackermann 氏のご協力に感謝いたします。