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

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

Настраивать

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

Объявить зависимости

В файле build.gradle вашего модуля объявите зависимость, используя последнюю версию библиотеки Credential Manager:

implementation "androidx.credentials:credentials:1.2.0-{latest}"

Объявить элемент службы в файле манифеста

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

<service android:name=".MyCredentialProviderService"
    android:enabled="true"
    android:exported="true"
    android:label="My Credential Provider"
    android:icon="<any drawable icon>"
    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>

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

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

В каталоге 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, такими как автозаполнение паролей и других данных. Эти поставщики могут использовать ту же внутреннюю инфраструктуру для хранения существующих типов учётных данных, одновременно расширяя её для поддержки других типов, включая пароли.

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

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

  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 , чтобы система могла добавить окончательный запрос к дополнительному намерению.
  • Ваш PendingIntent не должен быть создан с флагом PendingIntent.FLAG_ONE_SHOT так как пользователь может выбрать запись, вернуться и повторно выбрать ее, что приведет к двойному срабатыванию PendingIntent .
  • Ваш PendingIntent должен быть создан с уникальным кодом запроса, чтобы каждая запись могла иметь свой собственный соответствующий PendingIntent .

Обработка выбора записи для запросов на создание ключа доступа

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

    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 извлеките связанное намерение и передайте его в PendingIntentHandler.retrieveProviderGetCredentialRequest() .
  2. Извлеките GetPublicKeyCredentialOption из полученного выше запроса. Затем извлеките requestJson и clientDataHash из этого параметра.
  3. Извлеките credentialId из дополнительного намерения, которое было заполнено поставщиком учетных данных при настройке соответствующего 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. Для проверки пользователя используйте биометрическую подсказку (или другой метод подтверждения). Фрагмент кода ниже использует Android Biometric API.

  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 . Этот атрибут позволяет использовать намерение для открытия экрана настроек вашего приложения при нажатии пользователем имени провайдера в списке служб «Пароли, ключи доступа и автозаполнение» . Задайте значение этого атрибута равным имени активности, которая будет запускаться с экрана настроек.

<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: Экран настроек предпочтительной службы паролей, ключей доступа и автозаполнения.

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

Привилегированные приложения, такие как веб-браузеры, выполняют вызовы Credential Manager от имени других проверяющих сторон, устанавливая параметр origin в методах Credential Manager 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