Узнайте, как упростить процедуру аутентификации с помощью Credential Manager API в вашем приложении для 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 .
  2. Перейдите в модуль CredentialManager и откройте проект в Android Studio.

Давайте посмотрим исходное состояние приложения

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

  1. Запустите приложение.
  2. Вы видите главный экран с кнопкой регистрации и входа.
  3. Вы можете нажать «Зарегистрироваться», чтобы зарегистрироваться, используя ключ доступа или пароль.
  4. Вы можете нажать «Войти», чтобы войти в систему, используя ключ доступа и сохраненный пароль.

8c0019ff9011950a.jpeg

Чтобы понять, что такое ключи доступа и как они работают, см. раздел Как работают ключи доступа? .

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

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

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

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

Внутри диспетчера учетных данных -> приложение -> main -> java -> SignUpFragment.kt вы можете увидеть текстовое поле «имя пользователя» и кнопку для регистрации с использованием ключа доступа.

dcc5c529b310f2fb.jpeg

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

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

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

  • В своем приложении перейдите к методу SignUpFragment.kt , Find, 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 регистрации из ресурсов и возвращает 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 из ресурсов.
  • В этом json есть 4 поля, которые нужно заменить.
  • UserId должен быть уникальным, чтобы пользователь мог создать несколько ключей доступа (при необходимости). Вы заменяете <userId> сгенерированным идентификатором пользователя.
  • <challenge> также должен быть уникальным, чтобы вы могли создать случайное уникальное испытание. Этот метод уже есть в вашем коде.

Следующий фрагмент кода включает примеры параметров, которые вы получаете от сервера:

{
  "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

Объект проверяющей стороны соответствует данным вашего приложения. Ему необходимо:

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

pubKeyCredParams

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

excludeCredentials

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

authenticatorSelection.authenticatorAttachment

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

residentKey

укажите значение, «обязательное» для создания ключа доступа.

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

  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.

93022cb87c00f1fc.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, указывая, что (фиктивный) сервер сохранил открытый ключ для использования в будущем.
  • Вы устанавливаете флагSignedInThroughPasskeys как true, указывая, что вы входите в систему с помощью ключей доступа.
  • После входа в систему вы перенаправляете пользователя на главный экран.

Следующий фрагмент кода содержит примеры параметров, которые вы должны получить:

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

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

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

629001f4a778d4fb.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 аутентификации для получения всех ключей доступа, связанных с этой учетной записью пользователя.

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

Третий параметр имеет значение true, если вы предпочитаете, чтобы операция возвращалась немедленно при отсутствии доступных учетных данных вместо возврата к обнаружению удаленных учетных данных, и значение false (по умолчанию) в противном случае.

  • Найдите метод 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(). Для этого требуется список параметров учетных данных и контекст действия для отображения параметров на нижнем листе в этом контексте.
  • Как только запрос будет успешным, вы увидите на экране нижнюю таблицу со списком всех созданных учетных данных для связанной учетной записи.
  • Теперь пользователи могут подтвердить свою личность с помощью биометрии или блокировки экрана и т. д., чтобы подтвердить подлинность выбранных учетных данных.
  • Вы устанавливаете флагSignedInThroughPasskeys как true, указывая, что вы входите в систему с помощью ключей доступа. В противном случае ложь.
  • Вы управляете видимостью отображаемых представлений и обрабатываете исключения, если запрос по какой-либо причине завершается неудачей или неудачей. Здесь сообщения об ошибках регистрируются и отображаются в приложении в диалоговом окне ошибок. Вы можете проверить полные журналы ошибок с помощью Android Studio или команды отладки adb.
  • Теперь, наконец, вам нужно завершить процесс регистрации, отправив учетные данные открытого ключа на сервер и разрешив пользователю войти. Приложение получает объект учетных данных, содержащий открытый ключ, который вы можете отправить на сервер для аутентификации с помощью ключа доступа.

Здесь мы использовали макетный сервер, поэтому мы просто возвращаем 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, указывая, что (фиктивный) сервер подтвердил открытый ключ для будущего использования.
  • После входа в систему вы перенаправляете пользователя на главный экран.

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

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

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

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

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

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

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

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