Редакция 2025, 4 квартал: Узнайте, как упростить процесс аутентификации с помощью API Credential Manager в вашем Android-приложении.

1. Прежде чем начать

Традиционные решения для аутентификации создают ряд проблем, связанных с безопасностью и удобством использования.

Пароли широко используются, но...

  • Легко забывается
  • Пользователям необходимы знания для создания надежных паролей.
  • Злоумышленникам легко осуществлять фишинг, сбор и повторное использование вредоносного ПО.

В Android ведется работа над созданием API Credential Manager для упрощения процесса входа в систему и устранения угроз безопасности за счет поддержки паролей — отраслевого стандарта нового поколения для аутентификации без пароля .

Менеджер учетных данных объединяет поддержку паролей с традиционными методами аутентификации, такими как пароли, вход через Google и т. д.

Пользователи смогут создавать пароли и сохранять их в Google Password Manager, который будет синхронизировать эти пароли на всех устройствах Android, где пользователь авторизован. Для входа в систему необходимо создать пароль, связать его с учетной записью пользователя и сохранить его открытый ключ на сервере.

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

  • Регистрация: с использованием паролей и пароля.
  • Вход в систему: с использованием ключей доступа и сохраненного пароля.

Предварительные требования

  • Базовое понимание того, как запускать приложения в Android Studio.
  • Базовое понимание процесса аутентификации в приложениях Android.
  • Базовое понимание работы паролей .

Что вы узнаете

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

Что вам понадобится

Одна из следующих комбинаций устройств:

  • Устройство Android, работающее под управлением Android 9 или выше (для паролей) и Android 4.4 или выше (для аутентификации по паролю через API Credential Manager).
  • Устройство предпочтительно с биометрическим датчиком.
  • Обязательно зарегистрируйте блокировку экрана (биометрическую или иную).
  • Версия плагина Kotlin: 1.8.10

2. Настройка

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

Используйте тот же файл debug.keystore, что и в проекте, для сборки отладочной и релизной версий, чтобы проверить привязку цифровых активов имени пакета и хеш-суммы на вашем тестовом сервере. (Это уже сделано для примера приложения в файле build.gradle).

  1. Клонируйте этот репозиторий на свой ноутбук из ветки credman_codelab : https://github.com/android/identity-samples/tree/credman_codelab
git clone -b credman_codelab https://github.com/android/identity-samples.git
  1. Перейдите в модуль CredentialManager и откройте проект в Android Studio.

Давайте посмотрим на начальное состояние приложения.

Чтобы увидеть, как работает приложение в исходном состоянии, выполните следующие шаги:

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

7a6fe80f4cf877a8.jpeg

3. Добавить возможность регистрации с помощью паролей.

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

Теперь вам нужно будет создать пароль и зарегистрировать учетные данные пользователя с помощью биометрии/блокировки экрана.

Зарегистрируйтесь с помощью пароля

В коде файла CredentialManager/app/src/main/java/com/google/credentialmanager/sample/SignUpScreen.kt определено текстовое поле "имя пользователя" и кнопка для регистрации с помощью пароля.

1f4c50daa2551f1.jpeg

Определите лямбда-функцию createCredential() для использования в моделях представления.

Для работы объектов менеджера учетных данных требуется передача Activity , связанной со Screen. Однако операции менеджера учетных данных обычно запускаются в View Models, и не рекомендуется ссылаться на Activity внутри View Models. Поэтому мы определяем функции менеджера учетных данных в отдельном файле CredentialManagerUtil.kt и ссылаемся на них в соответствующих Screens, которые затем передают их в свои View Models в качестве обратных вызовов через лямбда-функции.

Найдите комментарий TODO в функции createCredential() в файле CredentialManagerUtil.kt и вызовите функцию CredentialManager.create() :

CredentialManagerUtil.kt

suspend fun createCredential(
    activity: Activity,
    request: CreateCredentialRequest
): CreateCredentialResponse {
    TODO("Create a CredentialManager object and call createCredential() with a CreateCredentialRequest")
    val credentialManager = CredentialManager.create(activity)
    return credentialManager.createCredential(activity, request)
}

Передайте запрос и другие данные JSON из ответа в вызов функции createPasskey().

Перед созданием ключа доступа необходимо запросить у сервера необходимую информацию для передачи в API диспетчера учетных данных во время вызова функции createCredential ().

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

  • В вашем приложении перейдите в файл SignUpViewModel.kt и найдите метод signUpWithPasskeys , где вы напишете логику для создания пароля и входа пользователя. Этот метод находится в том же классе.
  • Найдите блок комментариев TODO , чтобы create a CreatePublicKeyCredentialRequest() , и замените его следующим кодом:

SignUpViewModel.kt

TODO("Create a CreatePublicKeyCredentialRequest() with necessary registration json from server")
    val request = CreatePublicKeyCredentialRequest(
        jsonProvider.fetchRegistrationJson()
            .replace("<userId>", getEncodedUserId())
            .replace("<userName>", _username.value)
            .replace("<userDisplayName>", _username.value)
            .replace("<challenge>", getEncodedChallenge())
    )

Метод jsonProvider.fetchRegistrationJsonFromServer() считывает эмулированный JSON-ответ сервера PublicKeyCredentialCreationOptions из ресурсов и возвращает регистрационный JSON, который будет передан при создании ключа доступа. Мы заменяем некоторые значения-заполнители пользовательскими записями из нашего приложения и некоторыми фиктивными полями:

  • Этот JSON-файл неполный и содержит 4 поля, которые необходимо заменить.
  • Идентификатор пользователя (UserId) должен быть уникальным, чтобы пользователь мог создать несколько паролей (при необходимости). Замените <userId> сгенерированным значением userId .
  • <challenge> также должно быть уникальным, поэтому вы будете генерировать случайный уникальный запрос. Этот метод уже есть в вашем коде.

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

{
  "challenge": String,
  "rp": {
    "name": String,
    "id": String
  },
  "user": {
    "id": String,
    "name": String,
    "displayName": String
  },
  "pubKeyCredParams": [
    {
      "type": "public-key",
      "alg": -7
    },
    {
      "type": "public-key",
      "alg": -257
    }
  ],
  "timeout": 1800000,
  "attestation": "none",
  "excludeCredentials": [],
  "authenticatorSelection": {
    "authenticatorAttachment": "platform",
    "requireResidentKey": true,
    "residentKey": "required",
    "userVerification": "required"
  }
}

В следующей таблице приведены некоторые важные параметры объекта PublicKeyCredentialCreationOptions :

Параметры

Описания

challenge

Случайная строка, сгенерированная сервером, содержащая достаточно энтропии, чтобы её угадывание было невозможным. Её длина должна составлять не менее 16 байт. Это обязательное требование, но оно не используется во время регистрации, за исключением случаев аттестации .

user.id

Уникальный идентификатор пользователя. Это значение не должно содержать личную информацию, например, адреса электронной почты или имена пользователей. Хорошо подойдет случайное 16-байтовое значение, генерируемое для каждой учетной записи.

user.name

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

user.displayName

Это поле предназначено для необязательного, более удобного для пользователя названия учетной записи.

rp.id

Объект «Зависимая сторона» соответствует данным вашей заявки. Он обладает следующими атрибутами:

  • name (обязательно): название вашего приложения
  • ID (необязательный): соответствует домену или поддомену. Если отсутствует, используется текущий домен.
  • icon (необязательно).

pubKeyCredParams

Список допустимых алгоритмов и типов ключей. Этот список должен содержать как минимум один элемент.

excludeCredentials

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

authenticatorSelection.authenticatorAttachment

Указывает, следует ли подключать устройство к платформе, или нет, или же это не требуется. Установите это значение равным platform . Это означает, что вам нужен аутентификатор, встроенный в устройство платформы, и пользователю не будет предлагаться вставить, например, USB-ключ безопасности.

residentKey

Укажите значение, required для создания пароля.

Создать учетные данные

  1. После создания объекта CreatePublicKeyCredentialRequest() необходимо вызвать метод createCredential() с созданным запросом.

SignUpViewModel.kt

try {
   TODO("Call createCredential() with createPublicKeyCredentialRequest")
   createCredential(request)
   TODO("Complete the registration process after sending public key credential to your server and let the user in")

} catch (e: CreateCredentialException) {
   handlePasskeyFailure(e)
}

  • Вы управляете видимостью отображаемых представлений и обрабатываете исключения, если запрос не удается или выполняется по какой-либо причине. Здесь сообщения об ошибках записываются в журнал и отображаются в приложении в диалоговом окне ошибки. Вы можете просмотреть полный журнал ошибок через Android Studio или команду adb debug .

1ea8ace66135de1e.png

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

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

Внутри метода signUpWithPasskeys() найдите соответствующий комментарий и замените его следующим кодом:

SignUpViewModel.kt

try {
    createCredential(request)
    TODO("Complete the registration process after sending public key credential to your server and let the user in")
registerResponse()
    DataProvider.setSignedInThroughPasskeys(true)
    _navigationEvent.emit(NavigationEvent.NavigateToHome(signedInWithPasskeys = true))
} catch (e: CreateCredentialException) {
   handlePasskeyFailure(e)
}
  • registerResponse() возвращает true указывая на то, что фиктивный сервер сохранил открытый ключ для дальнейшего использования.
  • Установите флаг setSignedInThroughPasskeys в true .
  • После авторизации пользователь будет перенаправлен на главный экран.

Объект PublicKeyCredential может содержать больше полей. Пример таких полей показан ниже:

{
  "id": String,
  "rawId": String,
  "type": "public-key",
  "response": {
    "clientDataJSON": String,
    "attestationObject": String,
  }
}

В следующей таблице приведены некоторые важные параметры объекта PublicKeyCredential :

Параметры

Описания

id

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

rawId

Объект ArrayBuffer представляющий собой версию идентификатора учетных данных.

response.clientDataJSON

Объект ArrayBuffer содержит закодированные данные клиента.

response.attestationObject

Объект аттестации, закодированный в формате ArrayBuffer . Он содержит важную информацию, такую ​​как идентификатор участника проверки (RP ID), флаги и открытый ключ.

Запустите приложение, и вы сможете нажать кнопку « Зарегистрироваться с помощью паролей» и создать пароль.

4. Сохраните пароль в поставщике учетных данных.

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

Для сохранения учетных данных пароля пользователя в его поставщике паролей вам потребуется реализовать метод CreatePasswordRequest , который будет передаваться в createCredential() для сохранения пароля.

  • Найдите метод signUpWithPassword() и замените TODO вызовом createPassword :

SignUpViewModel.kt

TODO("CreatePasswordRequest with entered username and password")
    val passwordRequest = CreatePasswordRequest(_username.value, _password.value)

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

SignUpViewModel.kt

TODO("Create credential with created password request and log the user in")
    try {
        createCredential(passwordRequest)
        simulateServerDelayAndLogIn()
    } catch (e: Exception) {
        val errorMessage = "Exception Message : " + e.message
        Log.e("Auth", errorMessage)
        _passwordCreationError.value = errorMessage
        _isLoading.value = false
    }

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

5. Добавить возможность аутентификации с помощью ключа доступа или пароля.

Теперь вы готовы использовать его для безопасной аутентификации в вашем приложении.

76e81460b26f9798.png

Определите лямбда-функцию getCredential() для использования в моделях представления.

Как и прежде, мы будем вызывать getCredential() менеджера учетных данных в отдельном файле CredentialManagerUtil.kt для использования в соответствующих экранах и передачи в их модели представления в качестве обратных вызовов через лямбда-функции.

Найдите комментарий TODO в функции getCredential() в файле CredentialManagerUtil.kt и вызовите функцию CredentialManager.get() :

suspend fun getCredential(
    activity: Activity,
    request: GetCredentialRequest
): GetCredentialResponse {
    TODO("Create a CredentialManager object and call getCredential() with a GetCredentialRequest")
    val credentialManager = CredentialManager.create(activity)
    return credentialManager.getCredential(activity, request)
}

Получите ключ доступа и другие параметры для передачи в вызов функции getPasskey().

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

В ваших ресурсах ( AuthFromServer.txt ) уже есть фиктивный ответ, который возвращает такие параметры, как в этом практическом задании.

  • В вашем приложении перейдите в файл SignInViewModel.kt, найдите метод signInWithSavedCredentials , где вы напишете логику аутентификации с помощью сохраненного ключа доступа или пароля и предоставите пользователю доступ:
  • Создайте метод GetPublicKeyCredentialOption() с необходимыми параметрами для получения учетных данных от вашего поставщика учетных данных.

SignInViewModel.kt

TODO("Create a GetPublicKeyCredentialOption() with necessary authentication json from server")
    val getPublicKeyCredentialOption =
        GetPublicKeyCredentialOption(jsonProvider.fetchAuthJson(), null)

Метод fetchAuthJsonFromServer() считывает JSON-ответ аутентификации из ресурсов и возвращает JSON-ответ аутентификации для получения всех ключей доступа, связанных с данной учетной записью пользователя.

Второй параметр функции GetPublicKeyCredentialOption() — это clientDataHash — хеш, используемый для проверки подлинности проверяющей стороны. Устанавливайте его только в том случае, если вы задали GetCredentialRequest.origin . В примере приложения он установлен в null .

Примечание: Сервер в этом практическом задании разработан таким образом, чтобы возвращать JSON, максимально похожий на словарь PublicKeyCredentialRequestOptions , передаваемый в вызов метода getCredential() API. В следующем фрагменте кода приведены несколько примеров параметров, которые вы могли бы получить в реальном ответе:

{
  "challenge": String,
  "rpId": String,
  "userVerification": "",
  "timeout": 1800000
}

В следующей таблице приведены некоторые важные параметры объекта PublicKeyCredentialRequestOptions :

Параметры

Описания

challenge

Запрос, сгенерированный сервером и хранящийся в объекте ArrayBuffer . Это необходимо для предотвращения атак повторного воспроизведения. Никогда не принимайте один и тот же запрос в ответе дважды. Рассматривайте его как токен CSRF .

rpId

RP ID — это домен. Веб-сайт может указать либо свой домен, либо регистрируемый суффикс . Это значение должно совпадать с параметром rp.id , использованным при создании пароля.

  • Далее вам необходимо создать объект PasswordOption() для получения всех сохраненных паролей, хранящихся в вашем поставщике паролей, через API Credential Manager для этой учетной записи пользователя. Внутри метода getSavedCredentials() найдите TODO и замените его следующим кодом:

SigninViewModel.kt

TODO("Create a PasswordOption to retrieve all the associated user's password")

val getPasswordOption = GetPasswordOption()

Объедините их в запрос GetCredentialRequest .

SigninViewModel.kt

TODO("Combine requests into a GetCredentialRequest")
    val request = GetCredentialRequest(
        listOf(
            getPublicKeyCredentialOption,
            getPasswordOption
        )
    )

Получить учетные данные

Далее необходимо вызвать метод getCredential() со всеми указанными выше параметрами, чтобы получить соответствующие учетные данные:

SignInViewModel.kt

try {
    TODO("Call getCredential() with required credential options")
    val result = getCredential(request)

    val data = when (result.credential) {
        is PublicKeyCredential -> {
            val cred = result.credential as PublicKeyCredential
            DataProvider.setSignedInThroughPasskeys(true)
            "Passkey: ${cred.authenticationResponseJson}"
        }

        is PasswordCredential -> {
            val cred = result.credential as PasswordCredential
            DataProvider.setSignedInThroughPasskeys(false)
            "Got Password - User:${cred.id} Password: ${cred.password}"
        }

        is CustomCredential -> {
            //If you are also using any external sign-in libraries, parse them here with the utility functions provided.
            null
        }

        else -> null
    }

    TODO("Complete the authentication process after validating the public key credential to your server and let the user in.")
    } catch (e: Exception) {
        Log.e("Auth", "getCredential failed with exception: " + e.message.toString())
        _signInError.value =
            "An error occurred while authenticating: " + e.message.toString()
    } finally {
        _isLoading.value = false
    }
  • В метод getCredential() передается необходимая информация. Этот метод принимает список вариантов учетных данных и контекст активности для отображения этих вариантов в нижней панели в данном контексте.
  • После успешного выполнения запроса на экране появится всплывающее окно со списком всех созданных учетных данных для соответствующей учетной записи.
  • Теперь пользователи могут подтвердить свою личность с помощью биометрии или блокировки экрана и т.д., чтобы аутентифицировать выбранные учетные данные.
  • Если выбранные учетные данные являются PublicKeyCredential , установите флаг setSignedInThroughPasskeys в true . В противном случае установите его в false .

Приведённый ниже фрагмент кода содержит пример объекта PublicKeyCredential :

{
  "id": String
  "rawId": String
  "type": "public-key",
  "response": {
    "clientDataJSON": String
    "authenticatorData": String
    "signature": String
    "userHandle": String
  }
}

Приведенная ниже таблица не является исчерпывающей, но содержит важные параметры объекта PublicKeyCredential :

Параметры

Описания

id

Идентификатор аутентифицированного пароля, закодированный в Base64URL.

rawId

Объект ArrayBuffer представляющий собой версию идентификатора учетных данных.

response.clientDataJSON

Объект ArrayBuffer содержащий данные клиента. Это поле содержит информацию, такую ​​как запрос и источник, которые сервер RP должен проверить.

response.authenticatorData

Объект ArrayBuffer , содержащий данные аутентификатора. Это поле содержит такую ​​информацию, как идентификатор RP.

response.signature

Объект ArrayBuffer , содержащий подпись. Это значение является основой учетных данных и должно быть проверено на сервере.

response.userHandle

Объект ArrayBuffer , содержащий идентификатор пользователя, установленный при создании. Это значение можно использовать вместо идентификатора учетных данных, если серверу необходимо выбирать значения идентификаторов, которые он использует, или если бэкэнд хочет избежать создания индекса по идентификаторам учетных данных.

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

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

Внутри метода signInWithSavedCredentials() найдите соответствующий комментарий и замените его следующим кодом:

SignInViewModel.kt

TODO("Complete the authentication process after validating the public key credential to your server and let the user in.")
    if (data != null) {
        sendSignInResponseToServer()
        _navigationEvent.emit(NavigationEvent.NavigateToHome(signedInWithPasskeys = DataProvider.isSignedInThroughPasskeys()))
    }
  • sendSigninResponseToServer() возвращает true, указывая на то, что (фиктивный) сервер проверил открытый ключ для дальнейшего использования.
  • После авторизации пользователь будет перенаправлен на главный экран.

Запустите приложение и перейдите в раздел «Вход» > «Войти с помощью паролей/сохраненного пароля», затем попробуйте войти, используя сохраненные учетные данные.

Попробуйте!

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

6. Поздравляем!

Вы завершили этот практический урок! Если хотите посмотреть окончательное решение, оно доступно по ссылке: https://github.com/android/identity-samples/tree/main/CredentialManager

Если у вас возникнут вопросы, задайте их на StackOverflow, используя passkey .

Узнать больше