Интегрируйте Credential Manager с вашим решением поставщика учетных данных

Менеджер учетных данных — это набор API, представленных в Android 14, которые поддерживают несколько методов входа в систему, таких как имя пользователя и пароль, ключи доступа и решения для федеративного входа (например, вход через Google). При вызове API Менеджера учетных данных система Android собирает учетные данные от всех поставщиков учетных данных, установленных на устройстве. В этом документе описывается набор API, предоставляющих конечные точки интеграции для этих поставщиков учетных данных.

Настраивать

Прежде чем внедрять функциональность в ваш поставщик учетных данных, выполните шаги настройки, описанные в следующих разделах.

Объявление зависимостей

Добавьте следующие зависимости в скрипт сборки вашего модуля приложения, чтобы использовать последнюю версию библиотеки Credential Manager:

Котлин

dependencies {
    implementation("androidx.credentials:credentials:1.6.0-rc01")
}

Круто

dependencies {
    implementation "androidx.credentials:credentials:1.6.0-rc01"
}

Объявите элемент сервиса в файле манифеста.

В файле манифеста вашего приложения AndroidManifest.xml добавьте объявление <service> для класса службы, который наследует класс ` CredentialProviderService из библиотеки `androidx.credentials`, как показано в следующем примере.

<service android:name=".MyCredentialProviderService"
    android:enabled="true"
    android:exported="true"
    android:label="My Credential Provider"
    android:icon="@mipmap/ic_launcher"
    android:permission="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE"
    tools:targetApi="upside_down_cake">
    <intent-filter>
        <action android:name="android.service.credentials.CredentialProviderService"/>
    </intent-filter>
    <meta-data
        android:name="android.credentials.provider"
        android:resource="@xml/provider"/>
</service>

Разрешение и фильтр намерений, показанные в предыдущем примере, являются неотъемлемой частью работы потока Credential Manager. Разрешение необходимо для того, чтобы только система Android могла подключаться к этой службе. Фильтр намерений используется для обнаружения этой службы как поставщика учетных данных, который будет использоваться Credential Manager.

Объявить поддерживаемые типы учетных данных

В каталоге res/xml создайте новый файл с именем provider.xml . В этом файле объявите типы учетных данных, поддерживаемые вашим сервисом, используя константы, определенные для каждого типа учетных данных в библиотеке. В следующем примере сервис поддерживает как традиционные пароли, так и ключи доступа, для которых определены константы TYPE_PASSWORD_CREDENTIAL и TYPE_PUBLIC_KEY_CREDENTIAL :

<credential-provider xmlns:android="http://schemas.android.com/apk/res/android">
    <capabilities>
        <capability name="android.credentials.TYPE_PASSWORD_CREDENTIAL" />
        <capability name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" />
    </capabilities>
</credential-provider>

На предыдущих уровнях API поставщики учетных данных интегрировались с такими API, как автозаполнение паролей и других данных. Эти поставщики могли использовать ту же внутреннюю инфраструктуру для хранения существующих типов учетных данных, расширяя ее для поддержки других, включая ключи доступа.

Двухэтапный подход к взаимодействию с поставщиками услуг

Менеджер учетных данных взаимодействует с поставщиками учетных данных в два этапа:

  1. Первый этап — это этап начала/запроса , в ходе которого система связывается со службами поставщиков учетных данных и вызывает методы onBeginGetCredentialRequest() , onBeginCreateCredentialRequest() или onClearCredentialStateRequest() с запросами типа Begin… . Поставщики должны обрабатывать эти запросы и отвечать запросами типа Begin… , заполняя их записями, представляющими визуальные параметры для отображения в селекторе учетных записей. Каждая запись должна иметь установленный PendingIntent .
  2. После того, как пользователь выберет запись, начинается этап выбора , и срабатывает событие PendingIntent связанное с записью, вызывая соответствующую активность поставщика учетных данных. После завершения взаимодействия пользователя с этой активностью поставщик учетных данных должен установить ответ на результат активности, прежде чем завершить ее. Этот ответ затем отправляется клиентскому приложению, которое вызвало менеджер учетных данных.

Обработка создания паролей

Обработка запросов на создание паролей

Когда клиентское приложение хочет создать пароль и сохранить его в поставщике учетных данных, оно вызывает API createCredential . Чтобы обработать этот запрос в службе поставщика учетных данных таким образом, чтобы пароль действительно был сохранен в вашем хранилище, выполните шаги, описанные в следующих разделах.

  1. Переопределите метод onBeginCreateCredentialRequest() в вашем сервисе, расширенном от CredentialProviderService .
  2. Обработайте запрос BeginCreateCredentialRequest , создав соответствующий объект BeginCreateCredentialResponse и передав его в функцию обратного вызова.
  3. При создании объекта BeginCreateCredentialResponse добавьте необходимые CreateEntries . Каждый CreateEntry должен соответствовать учетной записи, в которой можно сохранить учетные данные, и должен иметь установленный параметр PendingIntent а также другие необходимые метаданные.

Следующий пример иллюстрирует, как выполнить эти шаги.

override fun onBeginCreateCredentialRequest(
    request: BeginCreateCredentialRequest,
    cancellationSignal: CancellationSignal,
    callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException>,
) {
    val response: BeginCreateCredentialResponse? = processCreateCredentialRequest(request)
    if (response != null) {
        callback.onResult(response)
    } else {
        callback.onError(CreateCredentialUnknownException())
    }
}

fun processCreateCredentialRequest(request: BeginCreateCredentialRequest): BeginCreateCredentialResponse? {
    when (request) {
        is BeginCreatePublicKeyCredentialRequest -> {
            // Request is passkey type
            return handleCreatePasskeyQuery(request)
        }
    }
    // Request not supported
    return null
}

private fun handleCreatePasskeyQuery(
    request: BeginCreatePublicKeyCredentialRequest
): BeginCreateCredentialResponse {

    // Adding two create entries - one for storing credentials to the 'Personal'
    // account, and one for storing them to the 'Family' account. These
    // accounts are local to this sample app only.
    val createEntries: MutableList<CreateEntry> = mutableListOf()
    createEntries.add(
        CreateEntry(
            PERSONAL_ACCOUNT_ID,
            createNewPendingIntent(PERSONAL_ACCOUNT_ID, CREATE_PASSKEY_INTENT)
        )
    )

    createEntries.add(
        CreateEntry(
            FAMILY_ACCOUNT_ID,
            createNewPendingIntent(FAMILY_ACCOUNT_ID, CREATE_PASSKEY_INTENT)
        )
    )

    return BeginCreateCredentialResponse(createEntries)
}

private fun createNewPendingIntent(accountId: String, action: String): PendingIntent {
    val intent = Intent(action).setPackage(PACKAGE_NAME)

    // Add your local account ID as an extra to the intent, so that when
    // user selects this entry, the credential can be saved to this
    // account
    intent.putExtra(EXTRA_KEY_ACCOUNT_ID, accountId)

    return PendingIntent.getActivity(
        applicationContext, UNIQUE_REQ_CODE,
        intent,
        (
            PendingIntent.FLAG_MUTABLE
                or PendingIntent.FLAG_UPDATE_CURRENT
            )
    )
}

При создании объекта PendingIntent необходимо соблюдать следующие правила:

  • Соответствующее действие должно быть настроено таким образом, чтобы отображать все необходимые биометрические запросы, подтверждения или выбор.
  • Любые необходимые данные, которые требуются поставщику при вызове соответствующей активности, должны быть указаны в качестве дополнительных параметров в намерении, используемом для создания вашего PendingIntent , например, accountId в процессе создания.
  • Ваш PendingIntent должен быть создан с флагом PendingIntent.FLAG_MUTABLE , чтобы система могла добавить окончательный запрос к дополнительному содержимому объекта Intent.
  • В вашем PendingIntent не следует использовать флаг PendingIntent.FLAG_ONE_SHOT , поскольку пользователь может выбрать запись, вернуться и выбрать ее снова, что приведет к двойному срабатыванию PendingIntent .
  • Ваш PendingIntent должен быть сформирован с уникальным кодом запроса, чтобы каждая запись могла иметь свой собственный PendingIntent .

Обработка выбора элементов для запросов на создание паролей

  1. Когда пользователь выбирает ранее заполненную CreateEntry , вызывается соответствующий PendingIntent и создается связанная с ним Activity поставщика.
  2. После вызова метода onCreate вашей Activity получите доступ к связанному с ней намерению и передайте его в класс PendingIntentHander , чтобы получить ProviderCreateCredentialRequest .
  3. Извлеките из запроса данные requestJson , callingAppInfo и clientDataHash .
  4. Извлеките локальный accountId из дополнительного параметра Intent. Это пример реализации, специфичный для конкретного приложения, и он не является обязательным. Этот accountId можно использовать для хранения учетных данных для данного конкретного accountId.
  5. Проверьте requestJson . В приведенном ниже примере используются локальные классы данных, такие как PublicKeyCredentialCreationOptions для преобразования входного JSON в структурированный класс в соответствии со спецификацией WebAuthn. В качестве поставщика учетных данных вы можете заменить это собственным парсером.
  6. Если звонок совершается из нативного приложения Android, проверьте ссылку на ресурс для вызывающего приложения.
  7. Отобразить запрос на аутентификацию. В приведенном ниже примере используется Android Biometric API.
  8. После успешной аутентификации сгенерируйте пару credentialId и ключей (key pair ).
  9. Сохраните закрытый ключ в локальной базе данных, указав в нем параметр callingAppInfo.packageName .
  10. Создайте JSON-ответ API веб-аутентификации , состоящий из открытого ключа и credentialId . В приведенном ниже примере используются локальные вспомогательные классы, такие как AuthenticatorAttestationResponse и FidoPublicKeyCredential , которые помогают создать JSON на основе упомянутой ранее спецификации. В качестве поставщика учетных данных вы можете заменить эти классы своими собственными конструкторами.
  11. Создайте объект CreatePublicKeyCredentialResponse используя сгенерированный выше JSON-файл.
  12. Добавьте CreatePublicKeyCredentialResponse в качестве дополнительного параметра в Intent с помощью PendingIntentHander.setCreateCredentialResponse() и установите для этого Intent результат выполнения Activity.
  13. Завершите задание.

Приведённый ниже пример кода иллюстрирует эти шаги. Этот код необходимо обработать в вашем классе Activity после вызова onCreate() .

override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
    super.onCreate(savedInstanceState, persistentState)
    // ...

    val request =
        PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)

    val accountId = intent.getStringExtra(CredentialsRepo.EXTRA_KEY_ACCOUNT_ID)
    if (request != null && request.callingRequest is CreatePublicKeyCredentialRequest) {
        val publicKeyRequest: CreatePublicKeyCredentialRequest =
            request.callingRequest as CreatePublicKeyCredentialRequest
        createPasskey(
            publicKeyRequest.requestJson,
            request.callingAppInfo,
            publicKeyRequest.clientDataHash,
            accountId
        )
    }
}

@SuppressLint("RestrictedApi")
fun createPasskey(
    requestJson: String,
    callingAppInfo: CallingAppInfo?,
    clientDataHash: ByteArray?,
    accountId: String?
) {
    val request = PublicKeyCredentialCreationOptions(requestJson)

    val biometricPrompt = BiometricPrompt(
        this,
        { }, // Pass in your own executor
        object : AuthenticationCallback() {
            override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                super.onAuthenticationError(errorCode, errString)
                finish()
            }

            override fun onAuthenticationFailed() {
                super.onAuthenticationFailed()
                finish()
            }

            @RequiresApi(VERSION_CODES.P)
            override fun onAuthenticationSucceeded(
                result: AuthenticationResult
            ) {
                super.onAuthenticationSucceeded(result)

                // Generate a credentialId
                val credentialId = ByteArray(32)
                SecureRandom().nextBytes(credentialId)

                // Generate a credential key pair
                val spec = ECGenParameterSpec("secp256r1")
                val keyPairGen = KeyPairGenerator.getInstance("EC")
                keyPairGen.initialize(spec)
                val keyPair = keyPairGen.genKeyPair()

                // Save passkey in your database as per your own implementation

                // Create AuthenticatorAttestationResponse object to pass to
                // FidoPublicKeyCredential

                val response = AuthenticatorAttestationResponse(
                    requestOptions = request,
                    credentialId = credentialId,
                    credentialPublicKey = getPublicKeyFromKeyPair(keyPair),
                    origin = appInfoToOrigin(callingAppInfo!!),
                    up = true,
                    uv = true,
                    be = true,
                    bs = true,
                    packageName = callingAppInfo.packageName
                )

                val credential = FidoPublicKeyCredential(
                    rawId = credentialId,
                    response = response,
                    authenticatorAttachment = "", // Add your authenticator attachment
                )
                val result = Intent()

                val createPublicKeyCredResponse =
                    CreatePublicKeyCredentialResponse(credential.json())

                // Set the CreateCredentialResponse as the result of the Activity
                PendingIntentHandler.setCreateCredentialResponse(
                    result,
                    createPublicKeyCredResponse
                )
                setResult(RESULT_OK, result)
                finish()
            }
        }
    )

    val promptInfo = BiometricPrompt.PromptInfo.Builder()
        .setTitle("Use your screen lock")
        .setSubtitle("Create passkey for ${request.rp.name}")
        .setAllowedAuthenticators(
            BiometricManager.Authenticators.BIOMETRIC_STRONG
            /* or BiometricManager.Authenticators.DEVICE_CREDENTIAL */
        )
        .build()
    biometricPrompt.authenticate(promptInfo)
}

@RequiresApi(VERSION_CODES.P)
fun appInfoToOrigin(info: CallingAppInfo): String {
    val cert = info.signingInfo.apkContentsSigners[0].toByteArray()
    val md = MessageDigest.getInstance("SHA-256")
    val certHash = md.digest(cert)
    // This is the format for origin
    return "android:apk-key-hash:${b64Encode(certHash)}"
}

Обрабатывать запросы на создание паролей.

Для обработки запросов на создание паролей выполните следующие действия:

  • Внутри метода processCreateCredentialRequest() упомянутого в предыдущем разделе, добавьте еще один case внутри блока switch для обработки запросов на ввод пароля.
  • При создании объекта BeginCreateCredentialResponse добавьте необходимые CreateEntries .
  • Каждая CreateEntry должна соответствовать учетной записи, в которой можно сохранить учетные данные, и должна иметь установленный параметр PendingIntent а также другие метаданные.

Следующий пример иллюстрирует, как выполнить эти шаги:

fun processCreateCredentialRequest(
    request: BeginCreateCredentialRequest
): BeginCreateCredentialResponse? {
    when (request) {
        is BeginCreatePublicKeyCredentialRequest -> {
            // Request is passkey type
            return handleCreatePasskeyQuery(request)
        }

        is BeginCreatePasswordCredentialRequest -> {
            // Request is password type
            return handleCreatePasswordQuery(request)
        }
    }
    return null
}

@RequiresApi(VERSION_CODES.M)
private fun handleCreatePasswordQuery(
    request: BeginCreatePasswordCredentialRequest
): BeginCreateCredentialResponse {
    val createEntries: MutableList<CreateEntry> = mutableListOf()

    // Adding two create entries - one for storing credentials to the 'Personal'
    // account, and one for storing them to the 'Family' account. These
    // accounts are local to this sample app only.
    createEntries.add(
        CreateEntry(
            PERSONAL_ACCOUNT_ID,
            createNewPendingIntent(PERSONAL_ACCOUNT_ID, CREATE_PASSWORD_INTENT)
        )
    )
    createEntries.add(
        CreateEntry(
            FAMILY_ACCOUNT_ID,
            createNewPendingIntent(FAMILY_ACCOUNT_ID, CREATE_PASSWORD_INTENT)
        )
    )

    return BeginCreateCredentialResponse(createEntries)
}

Обработка выбора элементов для запросов на создание паролей

Когда пользователь выбирает заполненную запись CreateEntry , выполняется соответствующий PendingIntent , который открывает связанную Activity. Получите доступ к связанному Intent, переданному в onCreate , и передайте его в класс PendingIntentHander чтобы получить метод ProviderCreateCredentialRequest .

Приведённый ниже пример иллюстрирует, как реализовать этот процесс. Этот код необходимо обработать в методе onCreate() вашего Activity.

val createRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
val accountId = intent.getStringExtra(CredentialsRepo.EXTRA_KEY_ACCOUNT_ID)

if (createRequest == null) {
    return
}

val request: CreatePasswordRequest = createRequest.callingRequest as CreatePasswordRequest

// Fetch the ID and password from the request and save it in your database
mDatabase.addNewPassword(
    PasswordInfo(
        request.id,
        request.password,
        createRequest.callingAppInfo.packageName
    )
)

// Set the final response back
val result = Intent()
val response = CreatePasswordResponse()
PendingIntentHandler.setCreateCredentialResponse(result, response)
setResult(Activity.RESULT_OK, result)
finish()

Обработка входа пользователя в систему

Вход пользователя в систему осуществляется в несколько этапов:

  • Когда клиентское приложение пытается авторизовать пользователя , оно подготавливает экземпляр GetCredentialRequest .
  • Платформа Android передает этот запрос всем соответствующим поставщикам учетных данных, привязываясь к этим службам.
  • Затем служба поставщика получает запрос BeginGetCredentialRequest , содержащий список объектов BeginGetCredentialOption , каждый из которых содержит параметры, которые можно использовать для получения соответствующих учетных данных.

Для обработки этого запроса в службе поставщика учетных данных выполните следующие шаги:

  1. Переопределите метод onBeginGetCredentialRequest() для обработки запроса. Обратите внимание, что если ваши учетные данные заблокированы, вы можете немедленно установить AuthenticationAction в ответе и вызвать функцию обратного вызова.

    private val unlockEntryTitle = "Authenticate to continue"
    
    override fun onBeginGetCredentialRequest(
        request: BeginGetCredentialRequest,
        cancellationSignal: CancellationSignal,
        callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,
    ) {
        if (isAppLocked()) {
            callback.onResult(
                BeginGetCredentialResponse(
                    authenticationActions = mutableListOf(
                        AuthenticationAction(
                            unlockEntryTitle, createUnlockPendingIntent()
                        )
                    )
                )
            )
            return
        }
        try {
            response = processGetCredentialRequest(request)
            callback.onResult(response)
        } catch (e: GetCredentialException) {
            callback.onError(GetCredentialUnknownException())
        }
    }
    

    Поставщики услуг, требующие разблокировки учетных данных перед возвратом каких-либо credentialEntries , должны настроить ожидающее намерение, которое перенаправит пользователя к процессу разблокировки в приложении:

    private fun createUnlockPendingIntent(): PendingIntent {
        val intent = Intent(UNLOCK_INTENT).setPackage(PACKAGE_NAME)
        return PendingIntent.getActivity(
            applicationContext, UNIQUE_REQUEST_CODE, intent,
            (
                PendingIntent.FLAG_MUTABLE
                    or PendingIntent.FLAG_UPDATE_CURRENT
                )
        )
    }
    
  2. Получите учетные данные из локальной базы данных и настройте их с помощью CredentialEntries , чтобы они отображались в селекторе. Для паролей вы можете установить credentialId в качестве дополнительного параметра в Intent, чтобы знать, к каким учетным данным он будет привязан, когда пользователь выберет эту запись.

    companion object {
        // These intent actions are specified for corresponding activities
        // that are to be invoked through the PendingIntent(s)
        private const val GET_PASSKEY_INTENT_ACTION = "PACKAGE_NAME.GET_PASSKEY"
        private const val GET_PASSWORD_INTENT_ACTION = "PACKAGE_NAME.GET_PASSWORD"
    }
    
    fun processGetCredentialRequest(
        request: BeginGetCredentialRequest
    ): BeginGetCredentialResponse {
        val callingPackageInfo = request.callingAppInfo
        val callingPackageName = callingPackageInfo?.packageName.orEmpty()
        val credentialEntries: MutableList<CredentialEntry> = mutableListOf()
    
        for (option in request.beginGetCredentialOptions) {
            when (option) {
                is BeginGetPasswordOption -> {
                    credentialEntries.addAll(
                        populatePasswordData(
                            callingPackageName,
                            option
                        )
                    )
                }
                is BeginGetPublicKeyCredentialOption -> {
                    credentialEntries.addAll(
                        populatePasskeyData(
                            callingPackageInfo,
                            option
                        )
                    )
                } else -> {
                    Log.i(TAG, "Request not supported")
                }
            }
        }
        return BeginGetCredentialResponse(credentialEntries)
    }
    
  3. Запросите учетные данные из вашей базы данных, создайте записи для ввода пароля и пароля.

    private fun populatePasskeyData(
        callingAppInfo: CallingAppInfo?,
        option: BeginGetPublicKeyCredentialOption
    ): List<CredentialEntry> {
        val passkeyEntries: MutableList<CredentialEntry> = mutableListOf()
        val request = PublicKeyCredentialRequestOptions(option.requestJson)
        // Get your credentials from database where you saved during creation flow
        val creds = getCredentialsFromInternalDb(request.rpId)
        val passkeys = creds.passkeys
        for (passkey in passkeys) {
            val data = Bundle()
            data.putString("credId", passkey.credId)
            passkeyEntries.add(
                PublicKeyCredentialEntry(
                    context = applicationContext,
                    username = passkey.username,
                    pendingIntent = createNewPendingIntent(
                        GET_PASSKEY_INTENT_ACTION,
                        data
                    ),
                    beginGetPublicKeyCredentialOption = option,
                    displayName = passkey.displayName,
                    icon = passkey.icon
                )
            )
        }
        return passkeyEntries
    }
    
    // Fetch password credentials and create password entries to populate to the user
    private fun populatePasswordData(
        callingPackage: String,
        option: BeginGetPasswordOption
    ): List<CredentialEntry> {
        val passwordEntries: MutableList<CredentialEntry> = mutableListOf()
    
        // Get your password credentials from database where you saved during
        // creation flow
        val creds = getCredentialsFromInternalDb(callingPackage)
        val passwords = creds.passwords
        for (password in passwords) {
            passwordEntries.add(
                PasswordCredentialEntry(
                    context = applicationContext,
                    username = password.username,
                    pendingIntent = createNewPendingIntent(
                        GET_PASSWORD_INTENT
                    ),
                    beginGetPasswordOption = option,
                    displayName = password.username,
                    icon = password.icon
                )
            )
        }
        return passwordEntries
    }
    
    private fun createNewPendingIntent(
        action: String,
        extra: Bundle? = null
    ): PendingIntent {
        val intent = Intent(action).setPackage(PACKAGE_NAME)
        if (extra != null) {
            intent.putExtra("CREDENTIAL_DATA", extra)
        }
    
        return PendingIntent.getActivity(
            applicationContext, UNIQUE_REQUEST_CODE, intent,
            (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
        )
    }
    
  4. После того как вы запросили и заполнили учетные данные, вам необходимо обработать этап выбора пользователем этих данных, будь то пароль или ключ доступа.

Обработка выбора пользователем паролей

  1. В методе onCreate соответствующей Activity получите связанный с ней Intent и передайте его в метод PendingIntentHandler.retrieveProviderGetCredentialRequest() .
  2. Извлеките параметр GetPublicKeyCredentialOption из полученного выше запроса. Затем извлеките из этого параметра requestJson и clientDataHash .
  3. Извлеките credentialId из параметра IntentExtra, который был заполнен поставщиком учетных данных при настройке соответствующего PendingIntent .
  4. Извлеките пароль из локальной базы данных, используя параметры запроса, указанные выше.
  5. Убедитесь, что пароль действителен, используя извлеченные метаданные и подтверждение пользователя.

    val getRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
    val publicKeyRequest = getRequest?.credentialOptions?.first() as GetPublicKeyCredentialOption
    
    val requestInfo = intent.getBundleExtra("CREDENTIAL_DATA")
    val credIdEnc = requestInfo?.getString("credId").orEmpty()
    
    // Get the saved passkey from your database based on the credential ID from the PublicKeyRequest
    val passkey = mDatabase.getPasskey(credIdEnc)
    
    // Decode the credential ID, private key and user ID
    val credId = b64Decode(credIdEnc)
    val privateKey = b64Decode(passkey.credPrivateKey)
    val uid = b64Decode(passkey.uid)
    
    val origin = appInfoToOrigin(getRequest.callingAppInfo)
    val packageName = getRequest.callingAppInfo.packageName
    
    validatePasskey(
        publicKeyRequest.requestJson,
        origin,
        packageName,
        uid,
        passkey.username,
        credId,
        privateKey
    )
    
  6. Для проверки личности пользователя отобразите запрос на биометрическую идентификацию (или используйте другой метод подтверждения). Приведенный ниже фрагмент кода использует API биометрической идентификации Android.

  7. После успешной аутентификации сформируйте JSON-ответ на основе спецификации W3 Web Authentication Assertion . В приведенном ниже фрагменте кода используются вспомогательные классы данных, такие как AuthenticatorAssertionResponse , для приема структурированных параметров и преобразования их в требуемый формат JSON. Ответ содержит цифровую подпись, полученную из закрытого ключа учетных данных WebAuthn. Сервер проверяющей стороны может проверить эту подпись для аутентификации пользователя перед входом в систему.

  8. Создайте объект PublicKeyCredential используя сгенерированный выше JSON, и установите его в итоговом GetCredentialResponse . Установите этот итоговый ответ в качестве результата данной операции.

Следующий пример иллюстрирует, как можно реализовать эти шаги:

val request = PublicKeyCredentialRequestOptions(requestJson)
val privateKey: ECPrivateKey = convertPrivateKey(privateKeyBytes)

val biometricPrompt = BiometricPrompt(
    this,
    { }, // Pass in your own executor
    object : BiometricPrompt.AuthenticationCallback() {
        override fun onAuthenticationError(
            errorCode: Int,
            errString: CharSequence
        ) {
            super.onAuthenticationError(errorCode, errString)
            finish()
        }

        override fun onAuthenticationFailed() {
            super.onAuthenticationFailed()
            finish()
        }

        override fun onAuthenticationSucceeded(
            result: BiometricPrompt.AuthenticationResult
        ) {
            super.onAuthenticationSucceeded(result)
            val response = AuthenticatorAssertionResponse(
                requestOptions = request,
                credentialId = credId,
                origin = origin,
                up = true,
                uv = true,
                be = true,
                bs = true,
                userHandle = uid,
                packageName = packageName
            )

            val sig = Signature.getInstance("SHA256withECDSA")
            sig.initSign(privateKey)
            sig.update(response.dataToSign())
            response.signature = sig.sign()

            val credential = FidoPublicKeyCredential(
                rawId = credId,
                response = response,
                authenticatorAttachment = "", // Add your authenticator attachment
            )
            val result = Intent()
            val passkeyCredential = PublicKeyCredential(credential.json())
            PendingIntentHandler.setGetCredentialResponse(
                result, GetCredentialResponse(passkeyCredential)
            )
            setResult(RESULT_OK, result)
            finish()
        }
    }
)

val promptInfo = BiometricPrompt.PromptInfo.Builder()
    .setTitle("Use your screen lock")
    .setSubtitle("Use passkey for ${request.rpId}")
    .setAllowedAuthenticators(
        BiometricManager.Authenticators.BIOMETRIC_STRONG
        /* or BiometricManager.Authenticators.DEVICE_CREDENTIAL */
    )
    .build()
biometricPrompt.authenticate(promptInfo)

Обработка выбора пользователем пароля для аутентификации

  1. В соответствующей активности получите доступ к намерению, переданному в onCreate , и извлеките ProviderGetCredentialRequest , используя PendingIntentHandler .
  2. Используйте GetPasswordOption в запросе, чтобы получить учетные данные пароля для входящего пакета.

    val getRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
    
    val passwordOption = getRequest?.credentialOptions?.first() as GetPasswordOption
    
    val username = passwordOption.allowedUserIds.first()
    // Fetch the credentials for the calling app package name
    val creds = mDatabase.getCredentials(callingAppInfo.packageName)
    val passwords = creds.passwords
    val it = passwords.iterator()
    var password = ""
    while (it.hasNext()) {
        val passwordItemCurrent = it.next()
        if (passwordItemCurrent.username == username) {
            password = passwordItemCurrent.password
            break
        }
    }
    
  3. После получения данных установите ответ для выбранных учетных данных пароля.

    // Set the response back
    val result = Intent()
    val passwordCredential = PasswordCredential(username, password)
    PendingIntentHandler.setGetCredentialResponse(
        result, GetCredentialResponse(passwordCredential)
    )
    setResult(Activity.RESULT_OK, result)
    finish()
    

Обработка выбора записи действия аутентификации

Как упоминалось ранее , поставщик учетных данных может установить AuthenticationAction если учетные данные заблокированы. Если пользователь выбирает этот пункт, вызывается Activity, соответствующая действию намерения, установленному в PendingIntent . Затем поставщик учетных данных может предложить процесс биометрической аутентификации или аналогичный механизм для разблокировки учетных данных. В случае успеха поставщик учетных данных должен создать BeginGetCredentialResponse , аналогично тому, как описана обработка входа пользователя выше , поскольку учетные данные теперь разблокированы. Этот ответ затем должен быть установлен с помощью метода PendingIntentHandler.setBeginGetCredentialResponse() , прежде чем подготовленное намерение будет установлено в качестве результата и Activity завершится.

Запросы на подтверждение учетных данных

Клиентское приложение может запросить очистку любого состояния, хранящегося для выбора учетных данных, например, поставщик учетных данных может запомнить ранее выбранные учетные данные и вернуть только их в следующий раз. Клиентское приложение вызывает этот API и ожидает, что этот «закрепленный» выбор будет очищен. Ваша служба поставщика учетных данных может обработать этот запрос, переопределив метод onClearCredentialStateRequest() :

override fun onClearCredentialStateRequest(
    request: ProviderClearCredentialStateRequest,
    cancellationSignal: CancellationSignal,
    callback: OutcomeReceiver<Void?, ClearCredentialException>
) {
    // Delete any maintained state as appropriate.
}

Чтобы пользователи могли открывать настройки вашего поставщика учетных данных с экрана «Пароли, ключи доступа и автозаполнение» , приложения, предоставляющие учетные данные, должны реализовать атрибут манифеста credential-provider settingsActivity в файле res/xml/provider.xml . Этот атрибут позволяет использовать Intent для открытия экрана настроек вашего приложения, если пользователь щелкнет по имени поставщика в списке служб «Пароли, ключи доступа и автозаполнение ». Установите значение этого атрибута равным имени действия, которое будет запущено с экрана настроек.

<credential-provider
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:settingsSubtitle="Example settings provider name"
    android:settingsActivity="com.example.SettingsActivity">
    <capabilities>
        <capability name="android.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" />
    </capabilities>
</credential-provider>
Схема, демонстрирующая функции кнопок «Изменить» и «Открыть».
Рисунок 1: Кнопка «Изменить» открывает существующее диалоговое окно выбора, позволяющее пользователю выбрать предпочтительного поставщика учетных данных. Кнопка «Открыть» запускает действие настроек, определенное в изменении манифеста, и открывает страницу настроек специально для этого поставщика.

Настройки намерений

Откройте настройки : Интент android.settings.CREDENTIAL_PROVIDER открывает экран настроек, где пользователь может выбрать предпочтительные и дополнительные поставщики учетных данных.

Экран настроек паролей, ключей доступа и автозаполнения
Рисунок 2: Экран настроек паролей, ключей доступа и автозаполнения.

Предпочтительная служба учетных данных : Интент ACTION_REQUEST_SET_AUTOFILL_SERVICE перенаправляет пользователя на экран выбора предпочтительного поставщика. Выбранный на этом экране поставщик становится предпочтительным поставщиком учетных данных и автозаполнения.

Схема, демонстрирующая функции кнопок «Изменить» и «Открыть».
Рисунок 3: Экран выбора предпочтительного сервиса для паролей, ключей доступа и настроек автозаполнения.

Получите список разрешенных приложений.

Привилегированные приложения, такие как веб-браузеры, выполняют вызовы диспетчера учетных данных от имени других заинтересованных сторон, устанавливая параметр origin в методах диспетчера учетных данных GetCredentialRequest() и CreatePublicKeyCredentialRequest() . Для обработки этих запросов поставщик учетных данных получает значение origin с помощью API getOrigin() .

Для получения origin ) приложению-поставщику учетных данных необходимо передать список привилегированных и доверенных вызывающих сторон в API androidx.credentials.provider.CallingAppInfo's getOrigin() . Этот список разрешенных объектов должен представлять собой допустимый объект JSON. origin возвращается, если имя packageName ) и отпечатки сертификатов, полученные из signingInfo совпадают с таковыми у приложения, найденного в списке privilegedAllowlist , переданном в API getOrigin() . После получения значения origin приложение-поставщик должно рассматривать это как привилегированный вызов и установить этот origin в данных клиента в объекте AuthenticatorResponse , вместо того чтобы вычислять origin с помощью подписи вызывающего приложения.

Если вы получаете origin , используйте clientDataHash , предоставленный непосредственно в CreatePublicKeyCredentialRequest() или GetPublicKeyCredentialOption() вместо того, чтобы собирать и хешировать clientDataJSON во время запроса подписи. Чтобы избежать проблем с разбором JSON, установите значение-заполнитель для clientDataJSON в ответе на аттестацию и утверждение. Google Password Manager использует общедоступный список разрешенных вызовов getOrigin() . Как поставщик учетных данных, вы можете использовать этот список или предоставить свой собственный в формате JSON, описанном API. Поставщик сам выбирает, какой список будет использоваться. Чтобы получить привилегированный доступ к учетным данным сторонних поставщиков, обратитесь к документации, предоставленной третьей стороной.

Включить поставщиков услуг на устройстве

Пользователям необходимо включить провайдера через настройки устройства > Пароли и учетные записи > Ваш провайдер > Включить или отключить .

fun createSettingsPendingIntent(): PendingIntent