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 アプリでパスキーのサポートを有効にするには、アプリが所有するウェブサイトとアプリを関連付けます。この関連付けを宣言する手順は次のとおりです。
デジタル アセット リンクの 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 にパス(末尾のスラッシュなど)を含めないでください。サブドメインは一致とは見なされません。つまり、domain を
www.example.com
と指定した場合、ドメインwww.counter.example.com
はアプリに関連付けられません。以下のフィールドでは Android アプリを識別します。
namespace
android_app
package_name
アプリのマニフェストで宣言されたパッケージ名。例: com.example.android
sha256_cert_fingerprints
アプリの署名証明書の SHA256 フィンガープリント。 デジタル アセット リンクの 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
ヘッダーを送信していることを確認します。ホストが Google にデジタル アセット リンク ファイルの取得を許可していることを確認します。
robots.txt
ファイルがある場合は、Googlebot エージェントに/.well-known/assetlinks.json
の取得を許可する必要があります。ほとんどのサイトでは、自動エージェントに/.well-known/
パス内のファイルの取得を許可するだけで、他のサービスがこれらのファイルのメタデータにアクセスできるようになります。User-agent: * Allow: /.well-known/
Android アプリで関連付けを宣言します。
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)
ユーザーのログインを行う
ユーザーのアカウントに関連付けられているすべてのパスキーとパスワードのオプションを取得する手順は次のとおりです。
パスワードとパスキーの認証オプションを初期化します。
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);
前の手順で取得したオプションを使用して、ログイン リクエストを作成します。
Kotlin
val getCredRequest = GetCredentialRequest( listOf(getPasswordOption, getPublicKeyCredentialOption) )
Java
GetCredentialRequest getCredRequest = new GetCredentialRequest.Builder() .addCredentialOption(getPasswordOption) .addCredentialOption(getPublicKeyCredentialOption) .build();
ログインフローを開始します。
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 開発者サービスのアプリ ストレージを消去すると、クールダウンをリセットできます。 また、テストデバイスまたはエミュレータでこのクールダウンを切り替えるには、電話アプリに移動してコード「 |
開始時のログインエラー: 8: 不明な内部エラーです。 |
|
CreatePublicKeyCredentialDomException: 受信リクエストを検証できません。 | アプリのパッケージ ID がサーバーに登録されていません。サーバーサイドの統合で登録を検証してください。 |