Android アプリで Credential Manager API を使用して認証プロセスを簡素化する方法を学習する

1. はじめに

従来の認証方法には、セキュリティやユーザビリティに関する課題が多く存在します。

パスワードは広く使用されていますが、次のような短所があります。

  • 忘れやすい。
  • 安全なパスワードを作成するための知識がユーザーに求められる。
  • 攻撃者によるフィッシング、ハーベスティング、リプレイ攻撃の対象になりやすい。

Android は、Credential Manager API の作成に取り組んできました。パスワードレス認証の次世代の業界標準であるパスキーをサポートすることで、ログイン体験を簡素化しセキュリティに関するリスクに対処します。

認証情報マネージャーは、パスキーのサポートを統合し、パスワードや「Google でログイン」といった従来の認証方法と組み合わせます。

ユーザーはパスキーを作成し、Google パスワード マネージャーに保存できるようになります。これにより、ログインしている Android デバイス間でパスキーが同期されます。パスキーはユーザー アカウントに関連付けて作成する必要があります。また、その公開鍵は、ユーザーがパスキーを使用してログインする前にサーバーに保存しておく必要があります。

この Codelab では、Credential Manager API を使ってパスキーおよびパスワードで登録する方法と、将来の認証プロセスでパスキーおよびパスワードを使用する方法を学びます。次の 2 つのフローが含まれます。

  • 登録: パスキーとパスワードを使います。
  • ログイン: パスキーと保存済みパスワードを使います。

前提条件

  • Android Studio でアプリを実行する方法についての基礎的な知識。
  • Android アプリでの認証フローについての基本的な知識。
  • パスキーに関する基本的な知識。

学習内容

  • パスキーを作成する方法。
  • パスワード マネージャーでパスワードを保存する方法。
  • パスキーまたは保存済みパスワードを使用してユーザーを認証する方法。

必要なもの

次のいずれかの組み合わせが必要です。

  • Android 9 以降(パスキー用)および Android 4.4 以降(Credential Manager API でのパスワード認証用)を搭載した Android デバイス。
  • デバイス(生体認証センサー付きが望ましい)。
  • 生体認証システム(または画面ロック)を必ず登録してください。
  • Kotlin プラグイン バージョン: 1.8.10。

2. セットアップする

  1. credman_codelab ブランチ(https://github.com/android/identity-samples/tree/credman_codelab)から、ノートパソコン上にこのリポジトリのクローンを作成します。
  2. CredentialManager モジュールを選び、Android Studio でプロジェクトを開きます。

アプリの初期状態を確認する

アプリの初期状態を確認するには、次の手順を実行します。

  1. アプリを起動します。
  2. 登録ボタンとログインボタンを備えたメイン画面が表示されます。
  3. 登録ボタンをクリックすると、パスキーまたはパスワードを使って登録できます。
  4. ログインボタンをクリックすると、パスキーまたは保存済みパスワードを使ってログインできます。

8c0019ff9011950a.jpeg

パスキーの詳細とその仕組みについては、パスキーの仕組みをご覧ください。

3. パスキーによる登録機能を追加する

Credential Manager API を使用する Android アプリで新しいアカウントに登録する際、ユーザーはアカウントのパスキーを作成できます。このパスキーは、ユーザーの選んだ認証情報プロバイダ上に安全にストアされ、将来のログインで使用されます。ユーザーは、毎回パスワードを入力する必要がなくなります。

次に、パスキーを作成し、生体認証データまたは画面ロックを使用してユーザーの認証情報を登録します。

パスキーで登録する

[CredentialManager] -> [app] -> [main] -> [java] -> [SignUpFragment.kt] 内に「username」というテキスト フィールドとパスキーで登録するためのボタンがあります。

dcc5c529b310f2fb.jpeg

本人確認とその他の JSON レスポンスを createPasskey() 呼び出しに渡す

パスキーを作成する前に、createCredential() 呼び出し時に、Credential Manager API に渡す必要情報を取得するようサーバーにリクエストする必要があります。

うれしいことに、アセット(RegFromServer.txt)内には、この Codelab のそうしたパラメータを返すモック レスポンスがあります。

  • アプリで SignUpFragment.kt に移動し signUpWithPasskeys メソッドを探します。パスキーを作成しユーザーを承認するロジックをこのメソッドに記述します。メソッドは同じクラスにあります。
  • コメント付き else ブロックを確認し、createPasskey() を呼び出して次のコードに置き換えます。

SignUpFragment.kt

//TODO : Call createPasskey() to signup with passkey

val data = createPasskey()

このメソッドは、画面上で有効なユーザー名が入力されると呼び出されます。

  • createPasskey() メソッド内で、返された必要なパラメータを含む CreatePublicKeyCredentialRequest() を作成する必要があります。

SignUpFragment.kt

//TODO create a CreatePublicKeyCredentialRequest() with necessary registration json from server

val request = CreatePublicKeyCredentialRequest(fetchRegistrationJsonFromServer())

fetchRegistrationJsonFromServer() はメソッドです。アセットからの登録 JSON レスポンスを読み取り、パスキー作成中に渡される登録 JSON を返します。

  • fetchRegistrationJsonFromServer() メソッドを探して TODO を次のコードに置き換え、JSON を返し、空の文字列の return 文を削除します。

SignUpFragment.kt

//TODO fetch registration mock response

val response = requireContext().readFromAsset("RegFromServer")

//Update userId,challenge, name and Display name in the mock
return response.replace("<userId>", getEncodedUserId())
   .replace("<userName>", binding.username.text.toString())
   .replace("<userDisplayName>", binding.username.text.toString())
   .replace("<challenge>", getEncodedChallenge())
  • アセットから登録 JSON を読み取ります。
  • この JSON の 4 つのフィールドを置き換えます。
  • UserId は、ユーザーが複数のパスキーを作成できるように一意にする必要があります(必要な場合)。<userId> を生成された userId に置き換えます。
  • <challenge> も、ランダムに異なる本人確認を生成できるように、一意にする必要があります。メソッドはすでにコード内にあります。

次のコード スニペットには、サーバーから受け取るサンプルのオプションが含まれています。

{
  "challenge": String,
  "rp": {
    "name": String,
    "id": String
  },
  "user": {
    "id": String,
    "name": String,
    "displayName": String
  },
  "pubKeyCredParams": [
    {
      "type": "public-key",
      "alg": -7
    },
    {
      "type": "public-key",
      "alg": -257
    }
  ],
  "timeout": 1800000,
  "attestation": "none",
  "excludeCredentials": [],
  "authenticatorSelection": {
    "authenticatorAttachment": "platform",
    "requireResidentKey": true,
    "residentKey": "required",
    "userVerification": "required"
  }
}

次の表に、PublicKeyCredentialCreationOptions ディクショナリの重要なパラメータを示します(すべてを網羅しているわけではありません)。

パラメータ

説明

challenge

サーバーが生成するランダムな文字列。推測を不可能にするのに十分な高さのエントロピーを備えています。16 バイト以上である必要があります。これは必須ですが、アテステーションを行う場合を除き、登録時には使用されません。

user.id

ユーザーの一意の ID。メールアドレスやユーザー名などの個人情報はこの値に含まれません。アカウントごとに生成するランダムな 16 バイトの値でも十分に機能します。

user.name

このフィールドには、メールアドレスやユーザー名など、ユーザーが認識できるアカウントの一意の識別子を格納します。この識別子はアカウント選択画面に表示されます(ユーザー名を使用する場合はパスワード認証の値と同じ値を使用します)。

user.displayName

これはオプションのフィールドであり、アカウントのわかりやすい名前を表します。ユーザー アカウントの人間が理解できる名前で、表示のみを目的としています。

rp.id

リライング パーティのエンティティはアプリの詳細情報に対応します。必要な情報は次のとおりです。

  • 名称(必須): アプリ名。
  • ID(省略可): ドメインまたはサブドメインに対応します。指定がない場合は現在のドメインが使用されます。
  • アイコン(省略可)。

pubKeyCredParams

公開鍵認証情報のパラメータは、許可されたアルゴリズムと鍵タイプのリストです。リストには、少なくとも 1 つの要素を含める必要があります。

excludeCredentials

デバイスを登録済みのユーザーが、別のデバイスを登録しようとすることがあります。単一の認証システム上で、同じアカウントについて複数の認証情報が作成されるのを制限するために、こうしたデバイスは無視できます。transports メンバーが指定されている場合、メンバーには、各認証情報の登録時に getTransports() 関数を呼び出した結果が含まれている必要があります。

authenticatorSelection.authenticatorAttachment

プラットフォームにデバイスを関連付けるべきかどうか、またはそれに関する要件がないかどうかを示します。「platform」に設定します。これは、プラットフォーム デバイスに組み込まれた認証システムを求めていることを示します。ユーザーは、USB セキュリティ キーなどの挿入を求められません。

residentKey

パスキーを作成するために「required」の値を示します。

認証情報を作成する

  1. CreatePublicKeyCredentialRequest() を作成すると、作成したリクエストで createCredential() を呼び出す必要があります。

SignUpFragment.kt

//TODO call createCredential() with createPublicKeyCredentialRequest

try {
   response = credentialManager.createCredential(
       requireActivity(),
       request
   ) as CreatePublicKeyCredentialResponse
} catch (e: CreateCredentialException) {
   configureProgress(View.INVISIBLE)
   handlePasskeyFailure(e)
}
  • 必要な情報を createCredential() に渡します。
  • リクエストが完了すると、パスキーを作成するよう求めるボトムシートが画面に表示されます。
  • ユーザーは、生体認証データや画面ロックで本人確認を行います。
  • レンダリングされたビューの表示設定を処理します。なんらかの理由でリクエストが失敗したり、成功しなかったりした場合は例外に対処します。エラー メッセージが記録され、エラー ダイアログ内のアプリに表示されます。Android Studio または adb デバッグ コマンドで、全エラーログを確認できます。

93022cb87c00f1fc.png

  1. 最後に、公開鍵認証情報をサーバーに送信してユーザーを承認することで、登録プロセスを完了する必要があります。アプリは、公開鍵が含まれた認証情報オブジェクトを受け取ります。この公開鍵をサーバーに送信してパスキーを登録できます。

ここでは、疑似サーバーを使用したため、今後の認証と検証のために、登録した公開鍵をサーバーが保存したことを示す true を返すだけです。

signUpWithPasskeys() メソッド内で、関連するコメントを探し、次のコードに置き換えます。

SignUpFragment.kt

//TODO : complete the registration process after sending public key credential to your server and let the user in

data?.let {
   registerResponse()
   DataProvider.setSignedInThroughPasskeys(true)
   listener.showHome()
}
  • registerResponse は true を返し、今後の使用のために(疑似)サーバーが公開鍵を保存したことを示します。
  • setSignedInThroughPasskeys フラグを true に設定し、パスキーでログインすることを示します。
  • ログイン後、ホーム画面にリダイレクトします。

次のコード スニペットには、受け取るべきオプションの例が含まれています。

{
  "id": String,
  "rawId": String,
  "type": "public-key",
  "response": {
    "clientDataJSON": String,
    "attestationObject": String,
  }
}

次の表に、PublicKeyCredential の重要なパラメータを示します(すべてを網羅しているわけではありません)。

パラメータ

説明

id

作成されたパスキーの、Base64URL でエンコードされた ID。この ID によって、ブラウザは認証時に一致するパスキーがデバイス内にあるかどうかを判別できます。この値は、バックエンドのデータベースに保存する必要があります。

rawId

認証情報 ID の ArrayBuffer オブジェクト バージョン。

response.clientDataJSON

エンコードされたクライアント データの ArrayBuffer オブジェクト。

response.attestationObject

エンコードされたアテステーション オブジェクトの ArrayBuffer。これには、RP ID、フラグ、公開鍵などの重要な情報が含まれます。

アプリを実行すると、パスキーで登録するためのボタンをクリックしてパスキーを作成できます。

4. 認証情報プロバイダにパスワードを保存する

このアプリでは、説明のため、ユーザー名とパスワードでの登録が登録画面にすでに実装されています。

ユーザー パスワードの認証情報をユーザーのパスワード プロバイダで保存するには、CreatePasswordRequest を実装し、createCredential() に渡してパスワードを保存します。

  • signUpWithPassword() メソッドを探し、TODO を createPassword 呼び出しに置き換えます。

SignUpFragment.kt

//TODO : Save the user credential password with their password provider

createPassword()
  • createPassword() メソッド内で、このようなパスワード リクエストを作成し、TODO を次のコードに置き換える必要があります。

SignUpFragment.kt

//TODO : CreatePasswordRequest with entered username and password

val request = CreatePasswordRequest(
   binding.username.text.toString(),
   binding.password.text.toString()
)
  • 次に、createPassword() メソッド内で、パスワード作成リクエストで認証情報を作成し、ユーザーのパスワード プロバイダにユーザーのパスワード認証情報を保存する必要があります。TODO を次のコードに置き換えます。

SignUpFragment.kt

//TODO : Create credential with created password request


try {
   credentialManager.createCredential(request, requireActivity()) as CreatePasswordResponse
} catch (e: Exception) {
   Log.e("Auth", " Exception Message : " + e.message)
}
  • ワンタップでパスワードを使って認証できるように、ユーザーのパスワード プロバイダでパスワード認証情報を保存できました。

5. パスキーまたはパスワードによる認証機能を追加する

アプリでの安全な認証方法としてパスキーやパスワードを使用する準備が整いました。

629001f4a778d4fb.png

getPasskey() 呼び出しに渡すチャレンジとその他のオプションを取得する

ユーザーに認証を求める前に、WebAuthn JSON で渡すパラメータ(チャレンジを含む)をサーバーにリクエストする必要があります。

アセット(AuthFromServer.txt)内には、この Codelab のそうしたパラメータを返すモック レスポンスがあります。

  • アプリで、SignInFragment.kt に移動し signInWithSavedCredentials メソッドを探します。このメソッドで、保存したパスキーやパスワードで認証するロジックを記述し、ユーザーを承認します。
  • コメント付き else ブロックを確認し、createPasskey() を呼び出して次のコードに置き換えます。

SignInFragment.kt

//TODO : Call getSavedCredentials() method to signin using passkey/password

val data = getSavedCredentials()
  • getSavedCredentials() メソッド内で、認証情報プロバイダから認証情報を取得するために必要なパラメータを含む GetPublicKeyCredentialOption() を作成する必要があります。

SigninFragment.kt

//TODO create a GetPublicKeyCredentialOption() with necessary registration json from server

val getPublicKeyCredentialOption =
   GetPublicKeyCredentialOption(fetchAuthJsonFromServer(), null)

この fetchAuthJsonFromServer() は、認証情報 JSON レスポンスをアセットから読み取り、認証情報 JSON を返して、このユーザー アカウントに関連付けられたパスキーをすべて取得するメソッドです。

2 つ目のパラメータである clientDataHash はリライング パーティを検証するために使用されるハッシュです。GetCredentialRequest.origin が設定されている場合にのみ設定されます。サンプルアプリでは null です。

3 つ目のパラメータは、利用可能な認証情報がないときに、リモートの認証情報を検出するようにフォールバックするのではなく、すぐにオペレーションを返したい場合は true にします。そうでない場合は false にします(デフォルト)。

  • fetchAuthJsonFromServer() メソッドを探して TODO を次のコードに置き換え、JSON を返し、空の文字列の return 文を削除します。

SignInFragment.kt

//TODO fetch authentication mock json

return requireContext().readFromAsset("AuthFromServer")

注: この Codelab のサーバーは、API の getCredential() 呼び出しに渡される PublicKeyCredentialRequestOptions ディクショナリにできるだけ近い JSON を返すように設計されています。次のコード スニペットには、受け取るオプションのサンプルがいくつか含まれています。

{
  "challenge": String,
  "rpId": String,
  "userVerification": "",
  "timeout": 1800000
}

次の表に、PublicKeyCredentialRequestOptions ディクショナリの重要なパラメータを示します(すべてを網羅しているわけではありません)。

パラメータ

説明

challenge

サーバーで生成されたチャレンジで、形式は ArrayBuffer オブジェクトです。これはリプレイ攻撃の防止に必要です。レスポンスで同じチャレンジを 2 回受け取ることはありません。これは CSRF トークンとみなすことができます。

rpId

RP ID はドメインです。ウェブサイトは、この値として自身のドメイン、または登録可能なサフィックスを指定できます。この値は、パスキーの作成時に使用された rp.id パラメータと一致する必要があります。

  • 次に、PasswordOption() オブジェクトを作成し、このユーザー アカウントについて Credential Manager API を介してパスワード プロバイダに保存されたパスワードをすべて取得する必要があります。getSavedCredentials() メソッド内で、TODO を探し次のコードに置き換えます。

SigninFragment.kt

//TODO create a PasswordOption to retrieve all the associated user's password

val getPasswordOption = GetPasswordOption()

認証情報を取得する

  • 次に、上記のすべてのオプションで getCredential() リクエストを呼び出し、関連する認証情報を取得します。

SignInFragment.kt

//TODO call getCredential() with required credential options

val result = try {
   credentialManager.getCredential(
       requireActivity(),
       GetCredentialRequest(
           listOf(
               getPublicKeyCredentialOption,
               getPasswordOption
           )  
     )
   )
} catch (e: Exception) {
   configureViews(View.INVISIBLE, true)
   Log.e("Auth", "getCredential failed with exception: " + e.message.toString())
   activity?.showErrorAlert(
       "An error occurred while authenticating through saved credentials. Check logs for additional details"
   )
   return null
}

if (result.credential is PublicKeyCredential) {
   val cred = result.credential as PublicKeyCredential
   DataProvider.setSignedInThroughPasskeys(true)
   return "Passkey: ${cred.authenticationResponseJson}"
}
if (result.credential is PasswordCredential) {
   val cred = result.credential as PasswordCredential
   DataProvider.setSignedInThroughPasskeys(false)
   return "Got Password - User:${cred.id} Password: ${cred.password}"
}
if (result.credential is CustomCredential) {
   //If you are also using any external sign-in libraries, parse them here with the utility functions provided.
}

  • 必要な情報を getCredential() に渡します。getCredential() は、認証情報オプションのリストとアクティビティ コンテキストを受け取り、そのコンテキスト内のボトムシートでオプションをレンダリングします。
  • リクエストが完了すると、ボトムシートが画面に表示され、関連アカウントで作成されたすべての認証情報が一覧表示されます。
  • ユーザーは、生体認証データや画面ロックで本人確認を行い、選択した認証情報を認証します。
  • setSignedInThroughPasskeys フラグを true に設定し、パスキーでログインすることを示します。それ以外の場合は、false です。
  • レンダリングされたビューの表示設定を処理します。なんらかの理由でリクエストが失敗したり、成功しなかったりした場合は例外に対処します。エラー メッセージが記録され、エラー ダイアログ内のアプリに表示されます。Android Studio または adb デバッグ コマンドで、全エラーログを確認できます。
  • 最後に、公開鍵認証情報をサーバーに送信してユーザーを承認することで、登録プロセスを完了する必要があります。アプリは、公開鍵が含まれた認証情報オブジェクトを受け取ります。この公開鍵をサーバーに送信してパスキーで認証できます。

ここでは、疑似サーバーを使用したため、サーバーが公開鍵を検証したことを示す true を返すだけです。

signInWithSavedCredentials() メソッド内で、関連するコメントを探し、次のコードに置き換えます。

SignInFragment.kt

//TODO : complete the authentication process after validating the public key credential to your server and let the user in.

data?.let {
   sendSignInResponseToServer()
   listener.showHome()
}
  • sendSigninResponseToServer() は、今後の使用のために(疑似)サーバーが公開鍵を検証したことを示す true を返します。
  • ログイン後、ホーム画面にリダイレクトします。

次のコード スニペットには、サンプルの PublicKeyCredential オブジェクトが含まれています。

{
  "id": String
  "rawId": String
  "type": "public-key",
  "response": {
    "clientDataJSON": String
    "authenticatorData": String
    "signature": String
    "userHandle": String
  }
}

次の表に、PublicKeyCredential オブジェクトの重要なパラメータを示します(すべてを網羅しているわけではありません)。

パラメータ

説明

id

認証済みパスキー認証情報の、Base64URL でエンコードされた ID。

rawId

認証情報 ID の ArrayBuffer オブジェクト バージョン。

response.clientDataJSON

クライアント データの ArrayBuffer オブジェクト。このフィールドには、RP サーバーで検証する必要があるチャレンジやオリジンなどの情報が含まれます。

response.authenticatorData

認証システムデータの ArrayBuffer オブジェクト。このフィールドには RP ID などの情報が含まれます。

response.signature

シグネチャの ArrayBuffer オブジェクト。この値は、認証情報の核となる情報であり、サーバーで検証する必要があります。

response.userHandle

作成時に設定されたユーザー ID を含む ArrayBuffer オブジェクト。サーバーが使用する ID 値をサーバー側で選択する必要がある場合、またはバックエンドで認証情報 ID のインデックス作成が行われないようにしたい場合は、認証情報 ID の代わりにこの値を使用できます。

アプリを実行して、[ログイン] -> [パスキー / 保存済みパスワードでログイン] に移動し、保存した認証情報を使用してログインを試します。

試してみる

Credential Manager API を使用して、パスキーの作成、認証情報マネージャーへのパスワード保存、パスキーまたは保存済みパスワードでの認証を Android アプリに実装しました。

6. 完了

これでこの Codelab は終了です。https://github.com/android/identity-samples/tree/main/CredentialManager で最終的な結果を確認できます。

ご不明な点がございましたら、passkey タグをつけて StackOverflow でご質問ください。

詳細