Миграция с FIDO2 на диспетчер учетных данных

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

Причины перехода с FIDO2 на Credential Manager

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

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

Чтобы начать миграцию с FIDO2 на Credential Manager, выполните следующие действия.

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

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

    plugins {
      //…
        id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
      //…
    }
    
  2. В build.gradle вашего проекта обновите зависимости, чтобы использовать диспетчер учетных данных и проверку подлинности служб 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 below:
      implementation 'androidx.credentials:credentials-play-services-auth:<latest-version>'
      // ...
    }
    
  3. Замените инициализацию FIDO инициализацией диспетчера учетных данных. Добавьте это объявление в класс, который вы используете для создания ключа доступа и методов входа:

    val credMan = CredentialManager.create(context)
    

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

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

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

    suspend fun registerRequest(sessionId: String ... {
        // ...
        .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(
            Request.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. Безопасно удалите из поля зрения все методы, которые обрабатывают средства запуска намерений и вызовы результатов действий.

  4. Поскольку registerRequest() теперь возвращает JSONObject , вам не нужно создавать PendingIntent . Замените возвращенное намерение на JSONObject . Обновите вызовы средства запуска намерений, чтобы они вызывали createCredential() из 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 as CreateCredentialRequest,
                    activity
                ) as CreatePublicKeyCredentialResponse
            } catch (e: CreateCredentialException) {
    
                showErrorAlert(activity, e)
    
                return null
            }
            return response
        }
    
  5. После успешного вызова отправьте ответ обратно на сервер. Запрос и ответ на этот вызов аналогичны реализации FIDO2, поэтому никаких изменений не требуется.

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

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

Рис. 2. Процесс аутентификации по ключу доступа в Credential Manager.
  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, поэтому никаких изменений не требуется.

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

,

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

Причины перехода с FIDO2 на Credential Manager

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

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

Чтобы начать миграцию с FIDO2 на Credential Manager, выполните следующие действия.

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

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

    plugins {
      //…
        id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
      //…
    }
    
  2. В build.gradle вашего проекта обновите зависимости, чтобы использовать диспетчер учетных данных и проверку подлинности служб 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 below:
      implementation 'androidx.credentials:credentials-play-services-auth:<latest-version>'
      // ...
    }
    
  3. Замените инициализацию FIDO инициализацией диспетчера учетных данных. Добавьте это объявление в класс, который вы используете для создания ключа доступа и методов входа:

    val credMan = CredentialManager.create(context)
    

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

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

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

    suspend fun registerRequest(sessionId: String ... {
        // ...
        .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(
            Request.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. Безопасно удалите из поля зрения все методы, которые обрабатывают средства запуска намерений и вызовы результатов действий.

  4. Поскольку registerRequest() теперь возвращает JSONObject , вам не нужно создавать PendingIntent . Замените возвращенное намерение на JSONObject . Обновите вызовы средства запуска намерений, чтобы они вызывали createCredential() из 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 as CreateCredentialRequest,
                    activity
                ) as CreatePublicKeyCredentialResponse
            } catch (e: CreateCredentialException) {
    
                showErrorAlert(activity, e)
    
                return null
            }
            return response
        }
    
  5. После успешного вызова отправьте ответ обратно на сервер. Запрос и ответ на этот вызов аналогичны реализации FIDO2, поэтому никаких изменений не требуется.

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

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

Рисунок 2. Процесс аутентификации по ключу доступа Credential Manager.
  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, поэтому никаких изменений не требуется.

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

,

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

Причины перехода с FIDO2 на Credential Manager

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

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

Чтобы начать миграцию с FIDO2 на Credential Manager, выполните следующие действия.

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

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

    plugins {
      //…
        id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
      //…
    }
    
  2. В build.gradle вашего проекта обновите зависимости, чтобы использовать диспетчер учетных данных и проверку подлинности служб 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 below:
      implementation 'androidx.credentials:credentials-play-services-auth:<latest-version>'
      // ...
    }
    
  3. Замените инициализацию FIDO инициализацией диспетчера учетных данных. Добавьте это объявление в класс, который вы используете для создания ключа доступа и методов входа:

    val credMan = CredentialManager.create(context)
    

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

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

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

    suspend fun registerRequest(sessionId: String ... {
        // ...
        .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(
            Request.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. Безопасно удалите из поля зрения все методы, которые обрабатывают средства запуска намерений и вызовы результатов действий.

  4. Поскольку registerRequest() теперь возвращает JSONObject , вам не нужно создавать PendingIntent . Замените возвращенное намерение на JSONObject . Обновите вызовы средства запуска намерений, чтобы они вызывали createCredential() из 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 as CreateCredentialRequest,
                    activity
                ) as CreatePublicKeyCredentialResponse
            } catch (e: CreateCredentialException) {
    
                showErrorAlert(activity, e)
    
                return null
            }
            return response
        }
    
  5. После успешного вызова отправьте ответ обратно на сервер. Запрос и ответ на этот вызов аналогичны реализации FIDO2, поэтому никаких изменений не требуется.

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

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

Рисунок 2. Процесс аутентификации по ключу доступа Credential Manager.
  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, поэтому никаких изменений не требуется.

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

,

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

Причины перехода с FIDO2 на Credential Manager

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

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

Чтобы начать миграцию с FIDO2 на Credential Manager, выполните следующие действия.

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

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

    plugins {
      //…
        id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
      //…
    }
    
  2. В build.gradle вашего проекта обновите зависимости, чтобы использовать диспетчер учетных данных и проверку подлинности служб 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 below:
      implementation 'androidx.credentials:credentials-play-services-auth:<latest-version>'
      // ...
    }
    
  3. Замените инициализацию FIDO инициализацией диспетчера учетных данных. Добавьте это объявление в класс, который вы используете для создания ключа доступа и методов входа:

    val credMan = CredentialManager.create(context)
    

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

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

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

    suspend fun registerRequest(sessionId: String ... {
        // ...
        .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(
            Request.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. Безопасно удалите из поля зрения все методы, которые обрабатывают средства запуска намерений и вызовы результатов действий.

  4. Поскольку registerRequest() теперь возвращает JSONObject , вам не нужно создавать PendingIntent . Замените возвращенное намерение на JSONObject . Обновите вызовы средства запуска намерений, чтобы они вызывали createCredential() из 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 as CreateCredentialRequest,
                    activity
                ) as CreatePublicKeyCredentialResponse
            } catch (e: CreateCredentialException) {
    
                showErrorAlert(activity, e)
    
                return null
            }
            return response
        }
    
  5. После успешного вызова отправьте ответ обратно на сервер. Запрос и ответ на этот вызов аналогичны реализации FIDO2, поэтому никаких изменений не требуется.

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

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

Рис. 2. Процесс аутентификации по ключу доступа в Credential Manager.
  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, поэтому никаких изменений не требуется.

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