Как перейти с FIDO2 на Менеджер учетных данных

Credential Manager API поддерживает ключи доступа, федеративную аутентификацию и сторонних поставщиков аутентификации. Это рекомендуемый API для аутентификации на устройствах Android, который обеспечивает безопасную и удобную среду для синхронизации учетных данных и управления ими. Если вы используете локальные учетные данные FIDO2, обновите приложение, чтобы оно поддерживало аутентификацию с помощью ключей доступа. Для этого интегрируйте Credential Manager API. В этом документе описано, как перенести проект с FIDO2 на Менеджер учетных данных.

Причины перейти с FIDO2 на Менеджер учетных данных

В большинстве случаев вам нужно перенести поставщика аутентификации приложения Android в Менеджер учетных данных. Вот несколько причин перейти на Менеджер учетных данных:

  • Поддержка ключей доступа. Менеджер учетных данных поддерживает ключи доступа – новый механизм аутентификации без пароля, который более безопасен и прост в использовании, чем пароли.
  • Поддержка нескольких способов входа. Менеджер учетных данных поддерживает несколько способов входа, включая пароли, ключи доступа и интегрированные способы входа. Это упрощает аутентификацию в приложении независимо от того, какой способ предпочитает пользователь.
  • Поддержка сторонних поставщиков учетных данных. В Android 14 и более поздних версиях Менеджер учетных данных поддерживает несколько сторонних поставщиков учетных данных. Это означает, что пользователи могут использовать свои существующие учетные данные от других поставщиков для входа в ваше приложение.
  • Единообразие. Менеджер учетных данных обеспечивает единообразие аутентификации в разных приложениях и при использовании разных механизмов входа. Это упрощает понимание и использование процесса аутентификации в вашем приложении.

Чтобы начать переход с FIDO2 на Менеджер учетных данных, выполните следующие действия.

Обновить зависимости

  1. Обновите плагин Kotlin в файле build.gradle проекта до версии 1.8.10 или более поздней.

      plugins {
        //…
          id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
        //…
      }
    
  2. В файле build.gradle проекта обновите зависимости, чтобы использовать последние версии библиотек Менеджера учетных данных и аутентификации сервисов Google Play.

      dependencies {
        // ...
        // Credential Manager:
        implementation 'androidx.credentials:credentials:<latest-version>'
    
        // Play Services Authentication:
        // Optional - needed for credentials support from play services, for devices running
        // Android 13 and lower:
        implementation 'androidx.credentials:credentials-play-services-auth:<latest-version>'
        // ...
      }
    
  3. Замените инициализацию FIDO на инициализацию Менеджера учетных данных. Добавьте это объявление в класс, который вы используете для создания ключа доступа и методов входа:

    val credMan = CredentialManager.create(context)
    

Создайте ключи доступа

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

Рисунок 1. На этом рисунке показано, как происходит обмен данными между приложением и сервером, когда ключ доступа создается с помощью Менеджера учетных данных.
  1. Чтобы получить необходимые параметры, которые отправляются в метод createCredential() во время создания ключа доступа, добавьте name("residentKey").value("required") (как описано в спецификации WebAuthn) в вызов сервера registerRequest().

    suspend fun registerRequest() {
        // ...
        val call = client.newCall(
            Builder()
                .method(
                    "POST",
                    jsonRequestBody {
                        name("attestation").value("none")
                        name("authenticatorSelection").objectValue {
                            name("residentKey").value("required")
                        }
                    }
                ).build()
        )
        // ...
    }
    
  2. Установите для return тип registerRequest() и все дочерние функции для JSONObject.

    suspend fun registerRequest(sessionId: String): ApiResult<JSONObject> {
        val call = client.newCall(
            Builder()
                .url("$BASE_URL/<your api url>")
                .addHeader("Cookie", formatCookie(sessionId))
                .method(
                    "POST",
                    jsonRequestBody {
                        name("attestation").value("none")
                        name("authenticatorSelection").objectValue {
                            name("authenticatorAttachment").value("platform")
                            name("userVerification").value("required")
                            name("residentKey").value("required")
                        }
                    }
                ).build()
        )
        val response = call.await()
        return response.result("Error calling the api") {
            parsePublicKeyCredentialCreationOptions(
                body ?: throw ApiException("Empty response from the api call")
            )
        }
    }
    
  3. Безопасно удалите из представления все методы, которые обрабатывают вызовы средства запуска intent и результатов действия.

  4. Поскольку registerRequest() теперь возвращает JSONObject, вам не нужно создавать PendingIntent. Замените возвращенный интент на JSONObject. Обновите вызовы средства запуска намерений, чтобы вызывать createCredential() из Credential Manager API. Вызовите метод API createCredential().

    suspend fun createPasskey(
        activity: Activity,
        requestResult: JSONObject
    ): CreatePublicKeyCredentialResponse? {
        val request = CreatePublicKeyCredentialRequest(requestResult.toString())
        var response: CreatePublicKeyCredentialResponse? = null
        try {
            response = credMan.createCredential(
                request = request as CreateCredentialRequest,
                context = activity
            ) as CreatePublicKeyCredentialResponse
        } catch (e: CreateCredentialException) {
    
            showErrorAlert(activity, e)
    
            return null
        }
        return response
    }
    
  5. После успешного вызова отправьте ответ обратно на сервер. Запрос и ответ для этого вызова аналогичны реализации FIDO2, поэтому никаких изменений не требуется.

Аутентификация с помощью ключей доступа

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

Рисунок 2. Процесс аутентификации с помощью ключа доступа в Менеджере учетных данных.
  1. Вызов запроса на вход, отправляемый на сервер для получения информации, которая должна быть передана в запросе getCredential(), такой же, как и в реализации FIDO2. Никаких изменений вносить не требуется.
  2. Как и в случае с вызовом запроса на регистрацию, возвращаемый ответ имеет формат JSONObject.

    /**
     * @param sessionId The session ID to be used for the sign-in.
     * @param credentialId The credential ID of this device.
     * @return a JSON object.
     */
    suspend fun signinRequest(): ApiResult<JSONObject> {
        val call = client.newCall(
            Builder().url(
                buildString {
                    append("$BASE_URL/signinRequest")
                }
            ).method("POST", jsonRequestBody {})
                .build()
        )
        val response = call.await()
        return response.result("Error calling /signinRequest") {
            parsePublicKeyCredentialRequestOptions(
                body ?: throw ApiException("Empty response from /signinRequest")
            )
        }
    }
    
    /**
     * @param sessionId The session ID to be used for the sign-in.
     * @param response The JSONObject for signInResponse.
     * @param credentialId id/rawId.
     * @return A list of all the credentials registered on the server,
     * including the newly-registered one.
     */
    suspend fun signinResponse(
        sessionId: String,
        response: JSONObject,
        credentialId: String
    ): ApiResult<Unit> {
    
        val call = client.newCall(
            Builder().url("$BASE_URL/signinResponse")
                .addHeader("Cookie", formatCookie(sessionId))
                .method(
                    "POST",
                    jsonRequestBody {
                        name("id").value(credentialId)
                        name("type").value(PUBLIC_KEY.toString())
                        name("rawId").value(credentialId)
                        name("response").objectValue {
                            name("clientDataJSON").value(
                                response.getString("clientDataJSON")
                            )
                            name("authenticatorData").value(
                                response.getString("authenticatorData")
                            )
                            name("signature").value(
                                response.getString("signature")
                            )
                            name("userHandle").value(
                                response.getString("userHandle")
                            )
                        }
                    }
                ).build()
        )
        val apiResponse = call.await()
        return apiResponse.result("Error calling /signingResponse") {
        }
    }
    
  3. Безопасно удалите из представления все методы, которые обрабатывают вызовы средства запуска намерений и результатов действий.

  4. Поскольку signInRequest() теперь возвращает JSONObject, вам не нужно создавать PendingIntent. Замените возвращенный интент на JSONObject и вызовите getCredential() из методов API.

    suspend fun getPasskey(
        activity: Activity,
        creationResult: JSONObject
    ): GetCredentialResponse? {
        Toast.makeText(
            activity,
            "Fetching previously stored credentials",
            Toast.LENGTH_SHORT
        )
            .show()
        var result: GetCredentialResponse? = null
        try {
            val request = GetCredentialRequest(
                listOf(
                    GetPublicKeyCredentialOption(
                        creationResult.toString(),
                        null
                    ),
                    GetPasswordOption()
                )
            )
            result = credMan.getCredential(activity, request)
            if (result.credential is PublicKeyCredential) {
                val publicKeycredential = result.credential as PublicKeyCredential
                Log.i("TAG", "Passkey ${publicKeycredential.authenticationResponseJson}")
                return result
            }
        } catch (e: Exception) {
            showErrorAlert(activity, e)
        }
        return result
    }
    
  5. После успешного вызова отправьте ответ обратно на сервер, чтобы проверить и аутентифицировать пользователя. Параметры запроса и ответа для этого вызова API аналогичны реализации FIDO2, поэтому никаких изменений не требуется.

Дополнительные ресурсы