Credential Manager を使用してユーザーのログインを行う

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

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

パスキーについて

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

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

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

前提条件

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

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

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

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

アプリ モジュールの build.gradle ファイルに次の依存関係を追加します。

Groovy

dependencies {
    implementation "androidx.credentials:credentials:1.0.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.0.0-alpha02"
}

Kotlin

dependencies {
    implementation("androidx.credentials:credentials:1.0.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.0.0-alpha02")
}

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

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

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

    [{
      "relation": ["delegate_permission/common.get_login_creds"],
      "target": {
        "namespace": "web",
        "site": "https://signin.example.com"
      }
     },
     {
      "relation": ["delegate_permission/common.get_login_creds"],
      "target": {
        "namespace": "android_app",
        "package_name": "com.example",
        "sha256_cert_fingerprints": [
          SHA_HEX_VALUE
        ]
      }
     }]
    

    relation フィールドは、宣言される関係を記述する 1 つ以上の文字列の配列です。アプリとサイトがログイン認証情報を共有することを宣言するには、文字列 delegate_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. Android アプリで関連付けを宣言します。

    1. asset_statements 文字列リソースを strings.xml ファイルに追加します。asset_statements 文字列は、読み込む assetlinks.json ファイルを指定する 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)

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

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

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

    Kotlin

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

    Java

    // Retrieves the user's saved password for your app from their
    // password provider.
    GetPasswordOption getPasswordOption = new GetPasswordOption();
    
    // Get passkeys from the user's public key credential provider.
    GetPublicKeyCredentialOption getPublicKeyCredentialOption =
            new GetPublicKeyCredentialOption(requestJson, preferImmediatelyAvailableCredentials);
  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(
                request = getCredRequest,
                activity = activity,
            )
            handleSignIn(result)
        } catch (e : GetCredentialException) {
            handleFailure(e)
        }
    }
    
    fun handleSignIn(result: GetCredentialResponse) {
        // Handle the successfully returned credential.
        val credential = result.credential
    
        when (credential) {
            is PublicKeyCredential -> {
                responseJson = credential.authenticationResponseJson
                fidoAuthenticateWithServer(responseJson)
            }
            is PasswordCredential -> {
                val username = credential.id
                val password = credential.password
                passwordAuthenticateWithServer(username, password)
            }
            else -> {
                // Catch any unrecognized credential type here.
                Log.e(TAG, "Unexpected type of credential")
            }
        }
    }

    Java

    credentialManager.getCredentialAsync(
        getCredRequest,
        activity,
        cancellationSignal,
        requireContext().getMainExecutor(),
        new CredentialManagerCallback<GetCredentialResponse, GetCredentialException>() {
            @Override
            public void onResult(GetCredentialResponse result) {
                // Handle the successfully returned credential.
                Credential credential = result.getCredential();
                if (credential instanceof PublicKeyCredential) {
                    String responseJson = ((PublicKeyCredential) credential)
                            .getAuthenticationResponseJson();
                    fidoAuthenticateToServer(responseJson);
                } else if (credential instanceof PasswordCredential) {
                    Log.d(TAG, "Got PasswordCredential");
                    String id = ((PasswordCredential) credential).getId();
                    String password = ((PasswordCredential) credential)
                            .getPassword();
                    firebaseSignInWithPassword(id, password);
                } else {
                  Log.e(
                      TAG,
                      "Unexpected type of credential: " +
                      credential.getClass().getName());
                }
            }
    
            @Override
            public void onError(GetCredentialException e) {
                Log.e(TAG, "Sign in failed with exception", e);
            }
        }
    );

次のスニペットは、パスキーの取得時に 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"
  }
}

登録フロー

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

パスキーを作成する

ユーザーがパスキーを登録して再認証に使用できるようにするには、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(
                request = createPublicKeyCredentialRequest,
                activity = activity,
            )
            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 CreateCustomCredentialException -> {
                // 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(
        createPublicKeyCredentialRequest,
        requireActivity(),
        cancellationSignal,
        requireContext().getMainExecutor(),
        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 CreateCustomCredentialException) {
                    // 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": "nhkQXfE59Jb97VyyNJkvDiXucMEvltduvcrDmGrODHY",
  "rp": {
    "name": "CredMan App Test",
    "id": "credential-manager-app-test.glitch.me"
  },
  "user": {
    "id": "2HzoHm_hY0CjuEESY9tY6-3SdjmNHOoNqaPDcZGzsr0",
    "name": "helloandroid@gmail.com",
    "displayName": "helloandroid@gmail.com"
  },
  "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"
  }
}

JSON レスポンスを処理する

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

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

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

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

Kotlin

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

    // Create credentials and handle result.
    coroutineScope.launch {
        try {
            val result =
                    credentialManager.createCredential(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(
        createPasswordRequest,
        requireActivity(),
        cancellationSignal,
        requireContext().getMainExecutor(),
        new CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException> {
            @Override
            public void onResult(CreateCredentialResponse result) {
                handleResult(result);
            }

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

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

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

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

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

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

開始時のログインエラー: 8: 不明な内部エラーです。
  1. デバイスが Google アカウントで適切にセットアップされていません。
  2. パスキー JSON が正しく作成されていません。
CreatePublicKeyCredentialDomException: 受信リクエストを検証できません。 アプリのパッケージ ID がサーバーに登録されていません。サーバーサイドの統合で登録を検証してください。