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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2. Настройте

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

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

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

Код внутри Credential Manager/app/main/java/SignUpFragment.kt определяет текстовое поле «имя пользователя» и кнопку для регистрации с помощью ключа доступа.

1f4c50daa2551f1.jpeg

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

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

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

  • В своем приложении перейдите к SignUpFragment.kt и найдите метод SignUpWithPasskeys , в котором вы напишете логику создания ключа доступа и входа пользователя. Этот метод можно найти в том же классе.
  • Проверьте блок else с комментарием для вызова createPasskey() и замените его следующим кодом:

SignUpFragment.kt

//TODO : Call createPasskey() to signup with passkey

val data = createPasskey()

Этот метод будет вызван, как только на экране появится действительное имя пользователя.

  • Внутри метода createPasskey() вам необходимо создать CreatePublicKeyCredentialRequest() с возвращаемыми необходимыми параметрами.

SignUpFragment.kt

//TODO create a CreatePublicKeyCredentialRequest() with necessary registration json from server

val request = CreatePublicKeyCredentialRequest(fetchRegistrationJsonFromServer())

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

  • Найдите метод fetchRegistrationJsonFromServer() и замените TODO следующим кодом, чтобы вернуть JSON, а также удалите оператор возврата пустой строки:

SignUpFragment.kt

//TODO fetch registration mock response

val response = requireContext().readFromAsset("RegFromServer")

//Update userId,challenge, name and Display name in the mock
return response.replace("<userId>", getEncodedUserId())
   .replace("<userName>", binding.username.text.toString())
   .replace("<userDisplayName>", binding.username.text.toString())
   .replace("<challenge>", getEncodedChallenge())
  • Этот 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() с созданным запросом.

SignUpFragment.kt

//TODO call createCredential() with createPublicKeyCredentialRequest

try {
   response = credentialManager.createCredential(
       requireActivity(),
       request
   ) as CreatePublicKeyCredentialResponse
} catch (e: CreateCredentialException) {
   configureProgress(View.INVISIBLE)
   handlePasskeyFailure(e)
}
  • Вы передаете необходимую информацию в createCredential() .
  • Как только запрос будет успешным, вы увидите нижнюю таблицу на экране, предлагающую создать ключ доступа.
  • Теперь пользователи могут подтвердить свою личность с помощью биометрии, блокировки экрана и т. д.
  • Вы управляете видимостью отображаемых представлений и обрабатываете исключения, если запрос по какой-либо причине завершается неудачей или неудачей. Здесь сообщения об ошибках регистрируются и отображаются в приложении в диалоговом окне ошибок. Вы можете проверить полные журналы ошибок через студию Android или команду adb debug .

1ea8ace66135de1e.png

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

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

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

SignUpFragment.kt

//TODO : complete the registration process after sending public key credential to your server and let the user in

data?.let {
   registerResponse()
   DataProvider.setSignedInThroughPasskeys(true)
   listener.showHome()
}
  • 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, флаги и открытый ключ.

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

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

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

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

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

SignUpFragment.kt

//TODO : Save the user credential password with their password provider

createPassword()
  • Внутри метода createPassword() вам необходимо создать запрос пароля следующим образом, замените TODO следующим кодом:

SignUpFragment.kt

//TODO : CreatePasswordRequest with entered username and password

val request = CreatePasswordRequest(
   binding.username.text.toString(),
   binding.password.text.toString()
)
  • Затем внутри метода createPassword() создайте учетные данные с помощью запроса на создание пароля и сохраните учетные данные пароля пользователя у своего поставщика паролей. Замените TODO следующим кодом:

SignUpFragment.kt

//TODO : Create credential with created password request


try {
   credentialManager.createCredential(requireActivity(), request) as CreatePasswordResponse
} catch (e: Exception) {
   Log.e("Auth", " Exception Message : " + e.message)
}
  • Теперь вы успешно сохранили учетные данные пароля у поставщика паролей пользователя для аутентификации с помощью пароля всего одним касанием.

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

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

76e81460b26f9798.png

Получите вызов и другие параметры для передачи вызову getPasskey().

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

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

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

SignInFragment.kt

//TODO : Call getSavedCredentials() method to signin using passkey/password

val data = getSavedCredentials()
  • Внутри метода getSavedCredentials() вам необходимо создать GetPublicKeyCredentialOption() с необходимыми параметрами, необходимыми для получения учетных данных от вашего поставщика учетных данных.

SigninFragment.kt

//TODO create a GetPublicKeyCredentialOption() with necessary registration json from server

val getPublicKeyCredentialOption =
   GetPublicKeyCredentialOption(fetchAuthJsonFromServer(), null)

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

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

  • Найдите метод fetchAuthJsonFromServer() и замените TODO следующим кодом, чтобы вернуть json, а также удалите оператор возврата пустой строки:

SignInFragment.kt

//TODO fetch authentication mock json

return requireContext().readFromAsset("AuthFromServer")

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

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

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

Параметры

Описания

challenge

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

rpId

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

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

SigninFragment.kt

//TODO create a PasswordOption to retrieve all the associated user's password

val getPasswordOption = GetPasswordOption()

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

  • Далее вам нужно вызвать запрос getCredential() со всеми вышеперечисленными параметрами, чтобы получить связанные учетные данные:

SignInFragment.kt

//TODO call getCredential() with required credential options

val result = try {
   credentialManager.getCredential(
       requireActivity(),
       GetCredentialRequest(
           listOf(
               getPublicKeyCredentialOption,
               getPasswordOption
           )
     )
   )
} catch (e: Exception) {
   configureViews(View.INVISIBLE, true)
   Log.e("Auth", "getCredential failed with exception: " + e.message.toString())
   activity?.showErrorAlert(
       "An error occurred while authenticating through saved credentials. Check logs for additional details"
   )
   return null
}

if (result.credential is PublicKeyCredential) {
   val cred = result.credential as PublicKeyCredential
   DataProvider.setSignedInThroughPasskeys(true)
   return "Passkey: ${cred.authenticationResponseJson}"
}
if (result.credential is PasswordCredential) {
   val cred = result.credential as PasswordCredential
   DataProvider.setSignedInThroughPasskeys(false)
   return "Got Password - User:${cred.id} Password: ${cred.password}"
}
if (result.credential is CustomCredential) {
   //If you are also using any external sign-in libraries, parse them here with the utility functions provided.
}

  • Вы передаете необходимую информацию в 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() найдите соответствующий комментарий и замените его следующим кодом:

SignInFragment.kt

//TODO : complete the authentication process after validating the public key credential to your server and let the user in.

data?.let {
   sendSignInResponseToServer()
   listener.showHome()
}
  • sendSigninResponseToServer() возвращает true, указывая, что (фиктивный) сервер подтвердил открытый ключ для будущего использования.
  • После входа в систему вы перенаправляете пользователя на главный экран.

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

Попробуйте это

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

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

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

Если у вас есть вопросы, задайте их на StackOverflow с помощью тега passkey .

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