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).
- Клонируйте этот репозиторий на свой ноутбук из ветки credman_codelab : https://github.com/android/identity-samples/tree/credman_codelab
git clone -b credman_codelab https://github.com/android/identity-samples.git
- Перейдите в модуль CredentialManager и откройте проект в Android Studio.
Давайте посмотрим на начальное состояние приложения.
Чтобы увидеть, как работает приложение в исходном состоянии, выполните следующие шаги:
- Запустите приложение.
- Вы видите главный экран с кнопками регистрации и входа в систему. Эти кнопки пока ничего не делают, но мы включим их функциональность в следующих разделах.

3. Добавить возможность регистрации с помощью паролей.
При регистрации новой учетной записи в приложении Android, использующем API Credential Manager, пользователи могут создать пароль для своей учетной записи. Этот пароль будет надежно храниться в выбранном пользователем поставщике учетных данных и использоваться для последующих входов в систему, не требуя от пользователя каждый раз вводить свой пароль.
Теперь вам нужно будет создать пароль и зарегистрировать учетные данные пользователя с помощью биометрии/блокировки экрана.
Зарегистрируйтесь с помощью пароля
В коде файла CredentialManager/app/src/main/java/com/google/credentialmanager/sample/SignUpScreen.kt определено текстовое поле "имя пользователя" и кнопка для регистрации с помощью пароля.

Определите лямбда-функцию 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 :
Параметры | Описания |
Случайная строка, сгенерированная сервером, содержащая достаточно энтропии, чтобы её угадывание было невозможным. Её длина должна составлять не менее 16 байт. Это обязательное требование, но оно не используется во время регистрации, за исключением случаев аттестации . | |
Уникальный идентификатор пользователя. Это значение не должно содержать личную информацию, например, адреса электронной почты или имена пользователей. Хорошо подойдет случайное 16-байтовое значение, генерируемое для каждой учетной записи. | |
В этом поле должен содержаться уникальный идентификатор учетной записи, который пользователь сможет распознать, например, адрес электронной почты или имя пользователя. Он будет отображаться в окне выбора учетной записи. (Если используется имя пользователя, используйте то же значение, что и при аутентификации по паролю.) | |
Это поле предназначено для необязательного, более удобного для пользователя названия учетной записи. | |
Объект «Зависимая сторона» соответствует данным вашей заявки. Он обладает следующими атрибутами:
| |
Список допустимых алгоритмов и типов ключей. Этот список должен содержать как минимум один элемент. | |
Пользователь, пытающийся зарегистрировать устройство, мог зарегистрировать и другие устройства. Чтобы ограничить создание нескольких учетных данных для одной и той же учетной записи на одном аутентификаторе, вы можете игнорировать эти устройства. Член | |
Указывает, следует ли подключать устройство к платформе, или нет, или же это не требуется. Установите это значение равным | |
| Укажите значение, |
Создать учетные данные
- После создания объекта
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.

- Наконец, вам необходимо завершить процесс регистрации. Приложение отправляет на сервер открытый ключ, который регистрирует его в качестве текущего пользователя.
Здесь мы использовали фиктивный сервер, поэтому просто возвращаем 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 :
Параметры | Описания |
Идентификатор созданного пароля, закодированный в Base64URL. Этот идентификатор помогает браузеру определить, есть ли соответствующий пароль на устройстве при аутентификации. Это значение должно храниться в базе данных на бэкэнде. | |
Объект | |
Объект | |
Объект аттестации, закодированный в формате |
Запустите приложение, и вы сможете нажать кнопку « Зарегистрироваться с помощью паролей» и создать пароль.
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. Добавить возможность аутентификации с помощью ключа доступа или пароля.
Теперь вы готовы использовать его для безопасной аутентификации в вашем приложении.

Определите лямбда-функцию 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 :
Параметры | Описания |
Запрос, сгенерированный сервером и хранящийся в объекте | |
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 :
Параметры | Описания |
Идентификатор аутентифицированного пароля, закодированный в Base64URL. | |
Объект | |
Объект | |
Объект | |
Объект | |
Объект |
- Наконец, необходимо завершить процесс аутентификации. Обычно после того, как пользователь завершит аутентификацию с помощью пароля, приложение отправляет на сервер открытый ключ, содержащий утверждение аутентификации , которое проверяет утверждение и аутентифицирует пользователя.
Здесь мы использовали фиктивный сервер, поэтому просто возвращаем 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 .