認証情報マネージャーを使用してユーザーのログインを行う

Credential Manager は、ユーザー名 / パスワード、パスキー、フェデレーション ログイン ソリューション(Google でログインなど)のような複数のログイン方法を単一の API でサポートする Jetpack API です。これにより、デベロッパーにとっては統合が容易になります。

さらに、ユーザーにとっては、複数の認証方法でログイン インターフェースが統一されるため、どの方法を選択するかにかかわらず、アプリへのログインがわかりやすく簡単になります。

このページでは、パスキーのコンセプトと、Credential Manager API を使用して、パスキーなどの認証ソリューションのクライアント側サポートを実装する手順について説明します。よくある質問のページでも、より詳細で具体的な質問に対する回答をご覧いただけます。

皆様からのフィードバックは、Credential Manager API の改善に欠かせない要素です。次のリンクを使用して、見つかった問題や API の改善に向けたアイデアを共有してください。

フィードバックする

パスキーについて

パスキーは、より安全で簡単なパスワードの代替方法です。パスキーにより、ユーザーは生体認証センサー(指紋認証や顔認証)、PIN、パターンを使用してアプリとウェブサイトにログインできます。これによってシームレスなログイン エクスペリエンスが実現し、ユーザーはユーザー名とパスワードを記憶する必要がなくなります。

パスキーは、FIDO Alliance と World Wide Web Consortium(W3C)が共同で開発した WebAuthn(Web Authentication)標準を採用しています。WebAuthn は公開鍵暗号を使用してユーザーを認証します。ユーザーがログインしているウェブサイトやアプリでは、公開鍵の確認と保管はできますが、秘密鍵が保管されることはありません。秘密鍵は秘密かつ安全に保管されます。この鍵は一意であり、ウェブサイトまたはアプリに関連付けられているため、パスキーはフィッシング攻撃から保護され、セキュリティが強化されます。

認証情報マネージャーにより、ユーザーはパスキーを作成して Google パスワード マネージャーに保存できます。

前提条件

Credential Manager を使用するには、このセクションの手順を行います。

最新のプラットフォーム バージョンを使用する

Credential Manager は、Android 4.4(API レベル 19)以降でサポートされます。

アプリに依存関係を追加する

アプリ モジュールのビルド スクリプトに次の依存関係を追加します。

Kotlin

dependencies {
    implementation("androidx.credentials:credentials:1.3.0-alpha02")

    // optional - needed for credentials support from play services, for devices running
    // Android 13 and below.
    implementation("androidx.credentials:credentials-play-services-auth:1.3.0-alpha02")
}

Groovy

dependencies {
    implementation "androidx.credentials:credentials:1.3.0-alpha02"

    // optional - needed for credentials support from play services, for devices running
    // Android 13 and below.
    implementation "androidx.credentials:credentials-play-services-auth:1.3.0-alpha02"
}

ProGuard ファイルでクラスを保持する

モジュールの proguard-rules.pro ファイルで、次のディレクティブを追加します。

-if class androidx.credentials.CredentialManager
-keep class androidx.credentials.playservices.** {
  *;
}

詳しくは、アプリの圧縮、難読化、最適化をご覧ください。

デジタル アセット リンクのサポートを追加する

Android アプリでパスキーのサポートを有効にするには、アプリが所有するウェブサイトとアプリとを関連付けます。この関連付けを宣言する手順は次のとおりです。

  1. デジタル アセット リンクの JSON ファイルを作成します。たとえば、ウェブサイト https://signin.example.com とパッケージ名 com.example の Android アプリがログイン認証情報を共有できることを宣言するには、以下の内容を含む assetlinks.json というファイルを作成します。

    [
      {
        "relation" : [
          "delegate_permission/common.handle_all_urls",
          "delegate_permission/common.get_login_creds"
        ],
        "target" : {
          "namespace" : "android_app",
          "package_name" : "com.example.android",
          "sha256_cert_fingerprints" : [
            SHA_HEX_VALUE
          ]
        }
      }
    ]
    

    relation フィールドは、宣言される関係を記述する 1 つ以上の文字列の配列です。アプリとサイトがログイン認証情報を共有することを宣言するには、関係に delegate_permission/handle_all_urlsdelegate_permission/common.get_login_creds を指定します。

    target フィールドは、宣言が適用されるアセットを指定するオブジェクトです。以下のフィールドではウェブサイトを識別します。

    namespace web
    site

    https://domain[:optional_port] の形式でのウェブサイトの URL(例: https://www.example.com)。

    domain は完全修飾する必要があります。HTTPS にポート 443 を使用する場合は、optional_port を省略する必要があります。

    site のターゲットにはルートドメインのみを指定できます。アプリの関連付けを特定のサブディレクトリに限定することはできません。URL にパス(末尾のスラッシュなど)を含めないでください。

    サブドメインは一致とは見なされません。つまり、domainwww.example.com と指定した場合、ドメイン www.counter.example.com はアプリに関連付けられません。

    以下のフィールドでは Android アプリを識別します。

    namespace android_app
    package_name アプリのマニフェストで宣言されたパッケージ名。例: com.example.android
    sha256_cert_fingerprints アプリの署名証明書の SHA256 フィンガープリント。
  2. デジタル アセット リンクの JSON ファイルを、ログイン ドメインの次の場所でホストします。

    https://domain[:optional_port]/.well-known/assetlinks.json
    

    たとえば、ログイン ドメインが signin.example.com の場合は、JSON ファイルを https://signin.example.com/.well-known/assetlinks.json でホストします。

    デジタル アセット リンク ファイルの MIME タイプは JSON にする必要があります。サーバーがレスポンスで Content-Type: application/json ヘッダーを送信していることを確認します。

  3. ホストが Google にデジタル アセット リンク ファイルの取得を許可していることを確認します。robots.txt ファイルがある場合は、Googlebot エージェントに /.well-known/assetlinks.json の取得を許可する必要があります。ほとんどのサイトでは、自動エージェントに /.well-known/ パス内のファイルの取得を許可すると、他のサービスがこれらのファイルのメタデータにアクセスできるようになります。

    User-agent: *
    Allow: /.well-known/
    
  4. マニフェスト ファイルの <application> に次の行を追加します。

    <meta-data android:name="asset_statements" android:resource="@string/asset_statements" />
    
  5. 認証情報マネージャーを介してパスワード ログインを使用している場合は、次の手順に沿ってマニフェストにデジタル アセット リンクを設定します。パスキーのみを使用している場合は、この手順は必要ありません。

    Android アプリで関連付けを宣言します。読み込む assetlinks.json ファイルを指定するオブジェクトを追加します。文字列で使用するアポストロフィや引用符はエスケープする必要があります。次に例を示します。

    <string name="asset_statements" translatable="false">
    [{
      \"include\": \"https://signin.example.com/.well-known/assetlinks.json\"
    }]
    </string>
    
    > GET /.well-known/assetlinks.json HTTP/1.1
    > User-Agent: curl/7.35.0
    > Host: signin.example.com
    
    < HTTP/1.1 200 OK
    < Content-Type: application/json
    

Credential Manager を構成する

CredentialManager オブジェクトを構成して初期化するには、次のようなロジックを追加します。

Kotlin

// Use your app or activity context to instantiate a client instance of
// CredentialManager.
val credentialManager = CredentialManager.create(context)

Java

// Use your app or activity context to instantiate a client instance of
// CredentialManager.
CredentialManager credentialManager = CredentialManager.create(context)

認証情報フィールドを指定する

Android 14 以降では、isCredential 属性を使用して、ユーザー名やパスワードのフィールドなどの認証情報フィールドを指定できます。この属性は、このビューが、認証情報マネージャーやサードパーティの認証情報プロバイダと連携することを目的とした認証情報フィールドであることを示します。これにより、自動入力サービスが適切な自動入力候補を提供できるようになります。アプリで Credential Manager API を使用すると、利用可能な認証情報が記載された認証情報マネージャーのボトムシートが表示されます。ユーザー名やパスワードの入力ダイアログを表示する必要はありません。

isCredential 属性を使用するには、関連するビューにこの属性を追加します。

<TextView
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:isCredential="true"
...
 />

ユーザーのログインを行う

ユーザーのアカウントに関連付けられているすべてのパスキーとパスワードのオプションを取得する手順は次のとおりです。

  1. パスワードとパスキーの認証オプションを初期化します。

    Kotlin

    // Retrieves the user's saved password for your app from their
    // password provider.
    val getPasswordOption = GetPasswordOption()
    
    // Get passkey from the user's public key credential provider.
    val getPublicKeyCredentialOption = GetPublicKeyCredentialOption(
        requestJson = requestJson
    )

    Java

    // Retrieves the user's saved password for your app from their
    // password provider.
    GetPasswordOption getPasswordOption = new GetPasswordOption();
    
    // Get passkey from the user's public key credential provider.
    GetPublicKeyCredentialOption getPublicKeyCredentialOption =
            new GetPublicKeyCredentialOption(requestJson);
  2. 前の手順で取得したオプションを使用してログイン リクエストを作成します。

    Kotlin

    val getCredRequest = GetCredentialRequest(
        listOf(getPasswordOption, getPublicKeyCredentialOption)
    )

    Java

    GetCredentialRequest getCredRequest = new GetCredentialRequest.Builder()
        .addCredentialOption(getPasswordOption)
        .addCredentialOption(getPublicKeyCredentialOption)
        .build();
  3. ログインフローを開始します。

    Kotlin

    coroutineScope.launch {
        try {
            val result = credentialManager.getCredential(
                // Use an activity-based context to avoid undefined system UI
                // launching behavior.
                context = activityContext,
                request = getCredRequest
            )
            handleSignIn(result)
        } catch (e : GetCredentialException) {
            handleFailure(e)
        }
    }
    
    fun handleSignIn(result: GetCredentialResponse) {
        // Handle the successfully returned credential.
        val credential = result.credential
    
        when (credential) {
            is PublicKeyCredential -> {
                val responseJson = credential.authenticationResponseJson
                // Share responseJson i.e. a GetCredentialResponse on your server to
                // validate and  authenticate
            }
            is PasswordCredential -> {
                val username = credential.id
                val password = credential.password
                // Use id and password to send to your server to validate
                // and authenticate
            }
          is CustomCredential -> {
              // If you are also using any external sign-in libraries, parse them
              // here with the utility functions provided.
              if (credential.type == ExampleCustomCredential.TYPE)  {
              try {
                  val ExampleCustomCredential = ExampleCustomCredential.createFrom(credential.data)
                  // Extract the required credentials and complete the authentication as per
                  // the federated sign in or any external sign in library flow
                  } catch (e: ExampleCustomCredential.ExampleCustomCredentialParsingException) {
                      // Unlikely to happen. If it does, you likely need to update the dependency
                      // version of your external sign-in library.
                      Log.e(TAG, "Failed to parse an ExampleCustomCredential", e)
                  }
              } else {
                // Catch any unrecognized custom credential type here.
                Log.e(TAG, "Unexpected type of credential")
              }
            } else -> {
                // Catch any unrecognized credential type here.
                Log.e(TAG, "Unexpected type of credential")
            }
        }
    }

    Java

    credentialManager.getCredentialAsync(
        // Use activity based context to avoid undefined
        // system UI launching behavior
        activity,
        getCredRequest,
        cancellationSignal,
        <executor>,
        new CredentialManagerCallback<GetCredentialResponse, GetCredentialException>() {
            @Override
            public void onResult(GetCredentialResponse result) {
                handleSignIn(result);
            }
    
            @Override
            public void onError(GetCredentialException e) {
                handleFailure(e);
            }
        }
    );
    
    public void handleSignIn(GetCredentialResponse result) {
        // Handle the successfully returned credential.
        Credential credential = result.getCredential();
        if (credential instanceof PublicKeyCredential) {
            String responseJson = ((PublicKeyCredential) credential).getAuthenticationResponseJson();
            // Share responseJson i.e. a GetCredentialResponse on your server to validate and authenticate
        } else if (credential instanceof PasswordCredential) {
            String username = ((PasswordCredential) credential).getId();
            String password = ((PasswordCredential) credential).getPassword();
            // Use id and password to send to your server to validate and authenticate
        } else if (credential instanceof CustomCredential) {
            if (ExampleCustomCredential.TYPE.equals(credential.getType())) {
                try {
                    ExampleCustomCredential customCred = ExampleCustomCredential.createFrom(customCredential.getData());
                    // Extract the required credentials and complete the
                    // authentication as per the federated sign in or any external
                    // sign in library flow
                } catch (ExampleCustomCredential.ExampleCustomCredentialParsingException e) {
                    // Unlikely to happen. If it does, you likely need to update the
                    // dependency version of your external sign-in library.
                    Log.e(TAG, "Failed to parse an ExampleCustomCredential", e);
                }
            } else {
                // Catch any unrecognized custom credential type here.
                Log.e(TAG, "Unexpected type of credential");
            }
        } else {
            // Catch any unrecognized credential type here.
            Log.e(TAG, "Unexpected type of credential");
        }
    }

次の例は、パスキーを取得したときに JSON リクエストをフォーマットする方法を示しています。

{
  "challenge": "T1xCsnxM2DNL2KdK5CLa6fMhD7OBqho6syzInk_n-Uo",
  "allowCredentials": [],
  "timeout": 1800000,
  "userVerification": "required",
  "rpId": "credential-manager-app-test.glitch.me"
}

次の例は、公開鍵認証情報の取得後の JSON レスポンスを示しています。

{
  "id": "KEDetxZcUfinhVi6Za5nZQ",
  "type": "public-key",
  "rawId": "KEDetxZcUfinhVi6Za5nZQ",
  "response": {
    "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiVDF4Q3NueE0yRE5MMktkSzVDTGE2Zk1oRDdPQnFobzZzeXpJbmtfbi1VbyIsIm9yaWdpbiI6ImFuZHJvaWQ6YXBrLWtleS1oYXNoOk1MTHpEdll4UTRFS1R3QzZVNlpWVnJGUXRIOEdjVi0xZDQ0NEZLOUh2YUkiLCJhbmRyb2lkUGFja2FnZU5hbWUiOiJjb20uZ29vZ2xlLmNyZWRlbnRpYWxtYW5hZ2VyLnNhbXBsZSJ9",
    "authenticatorData": "j5r_fLFhV-qdmGEwiukwD5E_5ama9g0hzXgN8thcFGQdAAAAAA",
    "signature": "MEUCIQCO1Cm4SA2xiG5FdKDHCJorueiS04wCsqHhiRDbbgITYAIgMKMFirgC2SSFmxrh7z9PzUqr0bK1HZ6Zn8vZVhETnyQ",
    "userHandle": "2HzoHm_hY0CjuEESY9tY6-3SdjmNHOoNqaPDcZGzsr0"
  }
}

利用可能な認証情報がない場合に例外を処理する

場合によっては、ユーザーが利用可能な認証情報を持っていない場合や、ユーザーが利用可能な認証情報の使用に同意しないことがあります。getCredential() が呼び出され、認証情報が見つからない場合は、NoCredentialException が返されます。このような場合は、コードで NoCredentialException インスタンスを処理する必要があります。

Kotlin

try {
  val credential = credentialManager.getCredential(credentialRequest)
} catch (e: NoCredentialException) {
  Log.e("CredentialManager", "No credential available", e)
}

Java

try {
  Credential credential = credentialManager.getCredential(credentialRequest);
} catch (NoCredentialException e) {
  Log.e("CredentialManager", "No credential available", e);
}

Android 14 以降では、getCredential() を呼び出す前に prepareGetCredential() メソッドを使用して、アカウント セレクタを表示するときのレイテンシを短縮できます。

Kotlin

val response = credentialManager.prepareGetCredential(
  GetCredentialRequest(
    listOf(
      <getPublicKeyCredentialOption>,
      <getPasswordOption>
    )
  )
}

Java

GetCredentialResponse response = credentialManager.prepareGetCredential(
  new GetCredentialRequest(
    Arrays.asList(
      new PublicKeyCredentialOption(),
      new PasswordOption()
    )
  )
);

prepareGetCredential() メソッドは UI 要素を呼び出しません。これは、後で getCredential() API を使用して残りの get-credential オペレーション(UI を含む)を起動できるように準備作業を行ううえで役立ちます。

キャッシュされたデータは PrepareGetCredentialResponse オブジェクトで返されます。既存の認証情報がある場合、結果はキャッシュに保存されます。後で残りの getCredential() API を起動して、キャッシュされたデータでアカウント セレクタを起動できます。

登録フロー

パスキーまたはパスワードのいずれかを使用して、認証するユーザーを登録できます。

パスキーを作成する

ユーザーがパスキーを登録して再認証に使用できるようにするには、CreatePublicKeyCredentialRequest オブジェクトを使用してユーザー認証情報を登録します。

Kotlin

fun createPasskey(requestJson: String, preferImmediatelyAvailableCredentials: Boolean) {
    val createPublicKeyCredentialRequest = CreatePublicKeyCredentialRequest(
        // Contains the request in JSON format. Uses the standard WebAuthn
        // web JSON spec.
        requestJson = requestJson,
        // Defines whether you prefer to use only immediately available
        // credentials, not hybrid credentials, to fulfill this request.
        // This value is false by default.
        preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials,
    )

    // Execute CreateCredentialRequest asynchronously to register credentials
    // for a user account. Handle success and failure cases with the result and
    // exceptions, respectively.
    coroutineScope.launch {
        try {
            val result = credentialManager.createCredential(
                // Use an activity-based context to avoid undefined system
                // UI launching behavior
                context = activityContext,
                request = createPublicKeyCredentialRequest,
            )
            handlePasskeyRegistrationResult(result)
        } catch (e : CreateCredentialException){
            handleFailure(e)
        }
    }
}

fun handleFailure(e: CreateCredentialException) {
    when (e) {
        is CreatePublicKeyCredentialDomException -> {
            // Handle the passkey DOM errors thrown according to the
            // WebAuthn spec.
            handlePasskeyError(e.domError)
        }
        is CreateCredentialCancellationException -> {
            // The user intentionally canceled the operation and chose not
            // to register the credential.
        }
        is CreateCredentialInterruptedException -> {
            // Retry-able error. Consider retrying the call.
        }
        is CreateCredentialProviderConfigurationException -> {
            // Your app is missing the provider configuration dependency.
            // Most likely, you're missing the
            // "credentials-play-services-auth" module.
        }
        is CreateCredentialUnknownException -> ...
        is CreateCredentialCustomException -> {
            // You have encountered an error from a 3rd-party SDK. If you
            // make the API call with a request object that's a subclass of
            // CreateCustomCredentialRequest using a 3rd-party SDK, then you
            // should check for any custom exception type constants within
            // that SDK to match with e.type. Otherwise, drop or log the
            // exception.
        }
        else -> Log.w(TAG, "Unexpected exception type ${e::class.java.name}")
    }
}

Java

public void createPasskey(String requestJson, boolean preferImmediatelyAvailableCredentials) {
    CreatePublicKeyCredentialRequest createPublicKeyCredentialRequest =
            // `requestJson` contains the request in JSON format. Uses the standard
            // WebAuthn web JSON spec.
            // `preferImmediatelyAvailableCredentials` defines whether you prefer
            // to only use immediately available credentials, not  hybrid credentials,
            // to fulfill this request. This value is false by default.
            new CreatePublicKeyCredentialRequest(
                requestJson, preferImmediatelyAvailableCredentials);

    // Execute CreateCredentialRequest asynchronously to register credentials
    // for a user account. Handle success and failure cases with the result and
    // exceptions, respectively.
    credentialManager.createCredentialAsync(
        // Use an activity-based context to avoid undefined system
        // UI launching behavior
        requireActivity(),
        createPublicKeyCredentialRequest,
        cancellationSignal,
        executor,
        new CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException>() {
            @Override
            public void onResult(CreateCredentialResponse result) {
                handleSuccessfulCreatePasskeyResult(result);
            }

            @Override
            public void onError(CreateCredentialException e) {
                if (e instanceof CreatePublicKeyCredentialDomException) {
                    // Handle the passkey DOM errors thrown according to the
                    // WebAuthn spec.
                    handlePasskeyError(((CreatePublicKeyCredentialDomException)e).getDomError());
                } else if (e instanceof CreateCredentialCancellationException) {
                    // The user intentionally canceled the operation and chose not
                    // to register the credential.
                } else if (e instanceof CreateCredentialInterruptedException) {
                    // Retry-able error. Consider retrying the call.
                } else if (e instanceof CreateCredentialProviderConfigurationException) {
                    // Your app is missing the provider configuration dependency.
                    // Most likely, you're missing the
                    // "credentials-play-services-auth" module.
                } else if (e instanceof CreateCredentialUnknownException) {
                } else if (e instanceof CreateCredentialCustomException) {
                    // You have encountered an error from a 3rd-party SDK. If
                    // you make the API call with a request object that's a
                    // subclass of
                    // CreateCustomCredentialRequest using a 3rd-party SDK,
                    // then you should check for any custom exception type
                    // constants within that SDK to match with e.type.
                    // Otherwise, drop or log the exception.
                } else {
                  Log.w(TAG, "Unexpected exception type "
                          + e.getClass().getName());
                }
            }
        }
    );
}

JSON リクエストをフォーマットする

パスキーを作成後、パスキーをユーザーのアカウントに関連付け、パスキーの公開鍵をサーバーに保存する必要があります。次のコード例は、パスキーの作成時に JSON リクエストをフォーマットする方法を示しています。

アプリにシームレスな認証を導入する方法について取り上げたこのブログ投稿では、パスキーの作成時とパスキーを使用した認証時に JSON リクエストをフォーマットする方法について説明しています。また、パスワードが効果的な認証ソリューションでない理由、既存の生体認証情報の活用方法、所有しているウェブサイトにアプリを関連付ける方法、パスキーを作成する方法、パスキーを使用した認証方法についても説明しています。

{
  "challenge": "abc123",
  "rp": {
    "name": "Credential Manager example",
    "id": "credential-manager-test.example.com"
  },
  "user": {
    "id": "def456",
    "name": "helloandroid@gmail.com",
    "displayName": "helloandroid@gmail.com"
  },
  "pubKeyCredParams": [
    {
      "type": "public-key",
      "alg": -7
    },
    {
      "type": "public-key",
      "alg": -257
    }
  ],
  "timeout": 1800000,
  "attestation": "none",
  "excludeCredentials": [
    {"id": "ghi789", "type": "public-key"},
    {"id": "jkl012", "type": "public-key"}
  ],
  "authenticatorSelection": {
    "authenticatorAttachment": "platform",
    "requireResidentKey": true,
    "residentKey": "required",
    "userVerification": "required"
  }
}

authenticatorAttachment の値を設定する

authenticatorAttachment パラメータを設定できるのは、認証情報の作成時のみです。platform または cross-platform を指定するか、値なしにします。ほとんどの場合は、値なしにすることをおすすめします。

  • platform: ユーザーの現在のデバイスを登録する、またはパスワードのユーザーにログイン後にパスキーへのアップグレードを求めるには、authenticatorAttachmentplatform に設定します。
  • cross-platform: 多要素認証情報を登録する場合によく使用される値で、パスキーのコンテキストでは使用されません。
  • 値なし: ユーザーが希望するデバイスで(アカウント設定などで)パスキーを柔軟に作成できるようにするには、ユーザーがパスキーの追加を選択する場合に authenticatorAttachment パラメータを指定しないでください。ほとんどの場合は、パラメータを指定しないでおくことをおすすめします。

重複するパスキーが作成されないようにする

オプションの excludeCredentials 配列に認証情報 ID をリストし、同じパスキーのプロバイダのパスキーがすでに存在する場合に新しいパスキーが作成されないようにします。

JSON レスポンスを処理する

次のコード スニペットは、公開鍵認証情報を作成するための JSON レスポンスの例を示しています。返された公開鍵認証情報を処理する方法をご確認ください。

{
  "id": "KEDetxZcUfinhVi6Za5nZQ",
  "type": "public-key",
  "rawId": "KEDetxZcUfinhVi6Za5nZQ",
  "response": {
    "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoibmhrUVhmRTU5SmI5N1Z5eU5Ka3ZEaVh1Y01Fdmx0ZHV2Y3JEbUdyT0RIWSIsIm9yaWdpbiI6ImFuZHJvaWQ6YXBrLWtleS1oYXNoOk1MTHpEdll4UTRFS1R3QzZVNlpWVnJGUXRIOEdjVi0xZDQ0NEZLOUh2YUkiLCJhbmRyb2lkUGFja2FnZU5hbWUiOiJjb20uZ29vZ2xlLmNyZWRlbnRpYWxtYW5hZ2VyLnNhbXBsZSJ9",
    "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViUj5r_fLFhV-qdmGEwiukwD5E_5ama9g0hzXgN8thcFGRdAAAAAAAAAAAAAAAAAAAAAAAAAAAAEChA3rcWXFH4p4VYumWuZ2WlAQIDJiABIVgg4RqZaJyaC24Pf4tT-8ONIZ5_Elddf3dNotGOx81jj3siWCAWXS6Lz70hvC2g8hwoLllOwlsbYatNkO2uYFO-eJID6A"
  }
}

クライアント データの JSON から送信元を確認する

origin はリクエストの送信元のアプリまたはウェブサイトを表し、フィッシング攻撃から保護するためにパスキーで使用されます。アプリのサーバーは、クライアント データの送信元を承認済みアプリとウェブサイトの許可リストと照合する必要があります。サーバーが、認識できない送信元のアプリまたはウェブサイトからリクエストを受け取った場合、そのリクエストは拒否されます。

ウェブケースの場合、origin は認証情報がログインした同一サイトの送信元を表します。たとえば、URL が https://www.example.com:8443/store?category=shoes#athletic の場合、originhttps://www.example.com:8443 です。

Android アプリの場合は、ユーザー エージェントが自動的に origin を呼び出し元アプリの署名に設定します。パスキー API の呼び出し元を検証するには、この署名がサーバーで一致しているかどうかを検証する必要があります。Android origin は、次のように APK 署名証明書の SHA-256 ハッシュから派生した URI です。

android:apk-key-hash:<sha256_hash-of-apk-signing-cert>

キーストアの署名証明書の SHA-256 ハッシュは、次のターミナル コマンドを実行して確認できます。

keytool -list -keystore <path-to-apk-signing-keystore>

SHA-256 ハッシュはコロン区切りの 16 進数形式(91:F7:CB:F9:D6:81…)であり、Android の origin 値は base64url でエンコードされています。次の Python の例では、ハッシュ形式を互換性のあるコロンで区切られた 16 進数形式に変換する方法を示しています。

import binascii
import base64
fingerprint = '91:F7:CB:F9:D6:81:53:1B:C7:A5:8F:B8:33:CC:A1:4D:AB:ED:E5:09:C5'
print("android:apk-key-hash:" + base64.urlsafe_b64encode(binascii.a2b_hex(fingerprint.replace(':', ''))).decode('utf8').replace('=', ''))

fingerprint の値は実際の値に置き換えます。次のような結果が表示されます。

android:apk-key-hash:kffL-daBUxvHpY-4M8yhTavt5QnFEI2LsexohxrGPYU

その文字列を、サーバーで許可されている送信元として照合できます。複数の署名証明書(デバッグとリリース用の証明書や複数のアプリなど)がある場合は、このプロセスを繰り返して、すべての送信元をサーバーで有効として受け入れます。

ユーザーのパスワードを保存する

ユーザーがアプリで認証フローのユーザー名とパスワードを指定した場合は、ユーザーの認証に使用できるユーザー認証情報を登録できます。そのためには、CreatePasswordRequest オブジェクトを作成します。

Kotlin

fun registerPassword(username: String, password: String) {
    // Initialize a CreatePasswordRequest object.
    val createPasswordRequest =
            CreatePasswordRequest(id = username, password = password)

    // Create credential and handle result.
    coroutineScope.launch {
        try {
            val result =
                credentialManager.createCredential(
                    // Use an activity based context to avoid undefined
                    // system UI launching behavior.
                    activityContext,
                    createPasswordRequest
                  )
            handleRegisterPasswordResult(result)
        } catch (e: CreateCredentialException) {
            handleFailure(e)
        }
    }
}

Java

void registerPassword(String username, String password) {
    // Initialize a CreatePasswordRequest object.
    CreatePasswordRequest createPasswordRequest =
        new CreatePasswordRequest(username, password);

    // Register the username and password.
    credentialManager.createCredentialAsync(
        // Use an activity-based context to avoid undefined
        // system UI launching behavior
        requireActivity(),
        createPasswordRequest,
        cancellationSignal,
        executor,
        new CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException>() {
            @Override
            public void onResult(CreateCredentialResponse result) {
                handleResult(result);
            }

            @Override
            public void onError(CreateCredentialException e) {
                handleFailure(e);
            }
        }
    );
}

認証情報の復元をサポートする

ユーザーが認証情報を保存したデバイスにアクセスできなくなった場合は、安全なオンライン バックアップからの復元が必要になることがあります。この認証情報復元プロセスをサポートする方法については、このブログ投稿 Google パスワード マネージャーでのパスキーのセキュリティの「アクセスの回復や新しいデバイスの追加」というセクションをご覧ください。

パスキー エンドポイントの well-known URL によるパスワード管理ツールのサポートを追加

パスワードと認証情報管理ツールとのシームレスな統合と将来的な互換性を確保するため、パスキー エンドポイントの well-known URL のサポートを追加することをおすすめします。これは、提携団体がパスキーのサポートを正式にアドバタイズし、パスキーの登録と管理のための直接リンクを提供するオープン プロトコルです。

  1. ウェブサイトに加えて Android アプリと iOS アプリを持つ https://example.com のリライング パーティの場合、well-known URL は https://example.com/.well-known/passkey-endpoints になります。
  2. この URL がクエリされた場合、レスポンスでは次のスキーマを使用しなければなりません。

    {
      "enroll": "https://example.com/account/manage/passkeys/create"
      "manage": "https://example.com/account/manage/passkeys"
    }
    
  3. このリンクをウェブではなくアプリで直接開くには、Android アプリリンクを使用します。

  4. 詳細については、GitHub の「Passkey Endpoints Well Known URL」の説明をご覧ください。

一般的なエラーのトラブルシューティング

一般的なエラーコードとその説明、およびその原因に関する情報を次の表に示します。

エラーコードと説明 原因
開始時のログインエラー: 16: キャンセルされたログイン プロンプトが多すぎるため、呼び出し元が一時的にブロックされました。

開発中に 24 時間のクールダウン期間が発生した場合は、Google Play 開発者サービスのアプリ ストレージを消去すると、クールダウンをリセットできます。

また、テストデバイスまたはエミュレータでこのクールダウンを切り替えるには、電話アプリに移動してコード「*#*#66382723#*#*」を入力します。電話アプリはすべての入力を消去します。アプリが終了する場合がありますが、確認メッセージは表示されません。

開始時のログインエラー: 8: 不明な内部エラーです。
  1. デバイスが Google アカウントで適切にセットアップされていません。
  2. パスキー JSON が正しく作成されていません。
CreatePublicKeyCredentialDomException: 受信リクエストを検証できません。 アプリのパッケージ ID がサーバーに登録されていません。サーバーサイドの統合で登録を検証してください。
CreateCredentialUnknownException: パスワードの保存中に、ワンタップでパスワード エラーの応答が表示されます。16: ユーザーが Android 自動入力でプロンプトを表示する可能性が高いため、パスワードの保存をスキップします。 このエラーは、Android 13 以前でのみ、かつ Google が自動入力プロバイダである場合にのみ発生します。この場合、ユーザーには自動入力の保存プロンプトが表示され、パスワードが Google パスワード マネージャーに保存されます。Google 自動入力を使用して保存された認証情報は、Credential Manager API と双方向で共有されます。そのため、このエラーは無視してかまいません。

参考情報

Credential Manager API とパスキーの詳細については、次のリソースをご覧ください。