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 にあるサーバーにリクエストを送信します。そちらで、同じアプリのウェブ版をお試しください。
ここでは、独自のバージョンのアプリを使用します。
- ウェブサイトの編集ページ(https://glitch.com/edit/#!/webauthn-codelab)に移動します。
- [リミックスして編集] を見つけます。ボタンをタップします。ボタンを押すと「フォーク」できますし、新しいプロジェクト URL を指定して独自のバージョンを続行します。
- 左上のプロジェクト名をコピーします(必要に応じて変更できます)。
- グリッチで
.env
ファイルのHOSTNAME
セクションに貼り付けます。
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] を選択します。チェックアウトします。
アプリをリミックスに関連付ける
gradle.properties
ファイルを開きます。ファイルの下部にあるホスト URL を、先ほど作成した Glitch リミックスに変更します。
// ...
# The URL of the server
host=https://<your-project-name>.glitch.me
これで、デジタル アセット リンクの設定は完了です。
4. アプリの現在の動作を見る
まず、アプリの動作を確認してみましょう。実行構成コンボボックスで必ず [app-start] を選択してください。[実行] をクリックします。(コンボボックスの横にある緑色の三角形のアイコン)をクリックして、接続された Android デバイスでアプリを起動します。
アプリを起動すると、ユーザー名を入力する画面が表示されます。これは UsernameFragment
です。このデモでは、アプリとサーバーは任意のユーザー名を受け入れます。何かを入力して [次へ] を押してください。
次に表示される画面は AuthFragment
です。ユーザーはパスワードを使用してログインできます。後ほど、ここに FIDO2 でログインする機能を追加します。ここでも、デモの目的から、アプリとサーバーはあらゆるパスワードを受け入れます。何かを入力して [ログイン] を押すだけです。
これはこのアプリの最後の画面、HomeFragment
です。今のところ、認証情報の空のリストだけがここに表示されます。[Reauth] を押すAuthFragment
に戻ります。[ログアウト] を押すUsernameFragment
に戻ります。「+」が付いたフローティング アクション ボタンこの段階では何も行われませんが、
新しい認証情報が必要です。
コーディングを始める前に、ここで役立つ手法を紹介します。Android Studio で TODO を押します。] をクリックします。この Codelab のすべての TODO のリストが表示されます。次のセクションでは、最初の TODO から始めます。
5. フィンガープリントを使用して認証情報を登録する
指紋を使用した認証を有効にするには、まず、プラットフォーム認証システム(指紋認証センサーなどの生体認証システムを使用してユーザーを確認する)を確認する、ユーザーが生成した認証情報を登録する必要があります。
前のセクションで見たように、フローティング アクション ボタンでは何も行われません。新しい認証情報を登録する方法を見ていきましょう。
サーバー 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
を持つバイト配列の追加フィールドがあります。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) を見つけます。
この registerResponse
メソッドは、UI で新しい認証情報が正常に生成された後、それをサーバーに送り返すために呼び出されます。
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 をクリックして新しい認証情報を登録できます。
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
を開きます。指紋のダイアログで、指紋によるログインを求められます。
おめでとうございます。Android で FIDO2 API を使用して登録とログインを行う方法を学習しました。
7. 完了
Codelab - 初めての Android FIDO2 API が完了しました。
学習した内容
- プラットフォーム認証システムを確認するユーザーを使用して認証情報を登録する方法。
- 登録済みの認証システムを使用してユーザーを認証する方法。
- 新しい認証システムを登録する際に利用できるオプション。
- 生体認証センサーを使用した再認証の UX に関するおすすめの方法。
次のステップ
- 同様のエクスペリエンスをウェブサイトで構築する方法を学びます。
詳しくは、初めての WebAuthn の Codelab をご覧ください。
リソース
FIDO Alliance の Yuriy Ackermann 氏のご協力に感謝いたします。