Ваш первый API FIDO2 для Android

1. Введение

Что такое API FIDO2?

API FIDO2 позволяет приложениям Android создавать и использовать надежные, проверенные учетные данные на основе открытого ключа для аутентификации пользователей. API предоставляет реализацию клиента WebAuthn , которая поддерживает использование роуминговых аутентификаторов BLE, NFC и USB (ключей безопасности), а также аутентификатор платформы, который позволяет пользователю проходить аутентификацию с помощью отпечатка пальца или блокировки экрана.

То, что ты построишь...

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

Чему вы научитесь...

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

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

  • Устройство Android со сканером отпечатков пальцев (даже без датчика отпечатков пальцев блокировка экрана может обеспечить эквивалентную функцию проверки пользователя)
  • ОС Android 7.0 или новее с последними обновлениями. Обязательно зарегистрируйте отпечаток пальца (или блокировку экрана).

2. Приступаем к настройке

Клонировать репозиторий

Посетите репозиторий GitHub.

https://github.com/android/codelab-fido2

$ git clone https://github.com/android/codelab-fido2.git

Что мы собираемся реализовать?

  • Разрешите пользователям зарегистрировать «аутентификатор платформы, проверяющий пользователя» (телефон Android с датчиком отпечатков пальцев сам будет действовать как таковой).
  • Позвольте пользователям повторно аутентифицироваться в приложении по отпечатку пальца.

Здесь вы можете предварительно просмотреть то, что собираетесь построить.

Начните свой проект Codelab

Готовое приложение отправляет запросы на сервер по адресу https://webauthn-codelab.glitch.me . Вы можете попробовать там веб-версию того же приложения.

c2234c42ba8a6ef1.png

Вы собираетесь работать над своей собственной версией приложения.

  1. Перейдите на страницу редактирования веб-сайта по адресу https://glitch.com/edit/#!/weauthn-codelab .
  2. Найдите кнопку «Remix to Edit» в правом верхнем углу. Нажав кнопку, вы можете «разветвить» код и продолжить работу с собственной версией вместе с новым URL-адресом проекта. 9ef108869885e4ce.png
  3. Скопируйте имя проекта вверху слева (вы можете изменить его по своему усмотрению). c91d0d59c61021a4.png
  4. Вставьте его в раздел HOSTNAME файла .env в Glitch. 889b55b1cf74b894.png

3. Свяжите свое приложение и веб-сайт со ссылками на цифровые активы.

Чтобы использовать API FIDO2 в приложении Android, свяжите его с веб-сайтом и поделитесь между ними учетными данными. Для этого используйте ссылки на цифровые активы . Вы можете объявить ассоциации, разместив файл JSON Digital Asset Links на своем веб-сайте и добавив ссылку на файл Digital Asset Link в манифест вашего приложения.

Разместите .well-known/assetlinks.json в своем домене.

Вы можете определить связь между вашим приложением и веб-сайтом, создав файл JSON и поместив его в .well-known/assetlinks.json . К счастью, у нас есть серверный код, который автоматически отображает файл assetlinks.json , просто добавляя следующие параметры среды в файл .env в виде сбоя:

  • ANDROID_PACKAGENAME : имя пакета вашего приложения (com.example.android.fido2).
  • ANDROID_SHA256HASH : SHA256 хеш вашего сертификата подписи.

Чтобы получить хэш SHA256 вашего сертификата подписи разработчика, используйте команду ниже. Пароль по умолчанию для хранилища ключей отладки — «android».

$ keytool -exportcert -list -v -alias androiddebugkey -keystore ~/.android/debug.keystore

Зайдя на https://<your-project-name>.glitch.me/.well-known/assetlinks.json , вы должны увидеть строку JSON, подобную этой:

[{
  "relation": ["delegate_permission/common.handle_all_urls", "delegate_permission/common.get_login_creds"],
  "target": {
    "namespace": "web",
    "site": "https://<your-project-name>.glitch.me"
  }
}, {
  "relation": ["delegate_permission/common.handle_all_urls", "delegate_permission/common.get_login_creds"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example.android.fido2",
    "sha256_cert_fingerprints": ["DE:AD:BE:EF:..."]
  }
}]

Откройте проект в Android Studio.

Нажмите «Открыть существующий проект Android Studio» на экране приветствия Android Studio.

Выберите папку «android» внутри хранилища.

1062875cf11ffb95.png

Свяжите приложение с вашим ремиксом

Откройте файл gradle.properties . В нижней части файла измените URL-адрес хоста на только что созданный вами ремикс Glitch.

// ...

# The URL of the server
host=https://<your-project-name>.glitch.me

На этом этапе ваша конфигурация Digital Asset Links должна быть полностью настроена.

4. Посмотрите, как приложение работает сейчас.

Давайте начнем с проверки того, как приложение работает сейчас. Обязательно выберите «app-start» в раскрывающемся списке конфигурации запуска. Нажмите «Выполнить» (зеленый треугольник рядом со списком), чтобы запустить приложение на подключенном устройстве Android.

29351fb97062b43c.png

Когда вы запустите приложение, вы увидите экран для ввода вашего имени пользователя. Это UsernameFragment . В целях демонстрации приложение и сервер принимают любое имя пользователя. Просто введите что-нибудь и нажмите «Далее».

бд9007614a9a3644.png

Следующий экран, который вы видите, — AuthFragment . Здесь пользователь может войти в систему с паролем. Позже мы добавим сюда функцию входа с помощью FIDO2. Опять же, в целях демонстрации приложение и сервер принимают любой пароль. Просто введите что-нибудь и нажмите «Войти».

d9caba817a0a99bd.png

Это последний экран приложения HomeFragment . На данный момент вы видите здесь только пустой список учетных данных. Нажатие «Reauth» вернет вас обратно в AuthFragment . Нажатие «Выход» вернет вас обратно в UsernameFragment . Плавающая кнопка действия со знаком «+» сейчас ничего не делает, но инициирует регистрацию

новые учетные данные после реализации процесса регистрации FIDO2.

1cfcc6c884020e37.png

Прежде чем приступить к кодированию, вот полезный прием. В Android Studio нажмите «TODO» внизу. Он покажет список всех TODO в этой кодовой лаборатории. В следующем разделе мы начнем с первого TODO.

e5a811bbc7cd7b30.png

5. Зарегистрируйте учетные данные с помощью отпечатка пальца.

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

37ce78fdf2759832.png

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

Вызовите API сервера: /auth/registerRequest

Откройте AuthRepository.kt и найдите TODO(1).

Здесь registerRequest — это метод, который вызывается при нажатии FAB. Мы хотели бы, чтобы этот метод вызывал API сервера /auth/registerRequest . API возвращает ApiResult со всеми параметрами PublicKeyCredentialCreationOptions , которые необходимы клиенту для создания новых учетных данных.

Затем мы можем вызвать getRegisterPendingIntent с параметрами. Этот API FIDO2 возвращает Android PendingIntent для открытия диалогового окна отпечатка пальца и создания новых учетных данных, и мы можем вернуть этот PendingIntent вызывающей стороне.

Тогда метод будет выглядеть, как показано ниже.

suspend fun registerRequest(): PendingIntent? {
  fido2ApiClient?.let { client ->
    try {
      val sessionId = dataStore.read(SESSION_ID)!!
      when (val apiResult = api.registerRequest(sessionId)) {
        ApiResult.SignedOutFromServer -> forceSignOut()
        is ApiResult.Success -> {
          if (apiResult.sessionId != null) {
            dataStore.edit { prefs ->
              prefs[SESSION_ID] = apiResult.sessionId
            }
          }
          val task = client.getRegisterPendingIntent(apiResult.data)
          return task.await()
        }
      }
    } catch (e: Exception) {
      Log.e(TAG, "Cannot call registerRequest", e)
    }
  }
  return null
}

Открыть диалог отпечатка пальца для регистрации

Откройте HomeFragment.kt и найдите TODO(2).

Здесь пользовательский интерфейс получает намерение из нашего AuthRepository . Здесь мы будем использовать член createCredentialIntentLauncher для запуска PendingIntent, который мы получили в результате предыдущего шага. Откроется диалоговое окно для создания учетных данных.

binding.add.setOnClickListener {
  lifecycleScope.launch {
    val intent = viewModel.registerRequest()
    if (intent != null) {
      createCredentialIntentLauncher.launch(
        IntentSenderRequest.Builder(intent).build()
      )
    }
  }
}

Получите ActivityResult с новыми учетными данными.

Откройте HomeFragment.kt и найдите TODO(3).

Этот метод handleCreateCredentialResult вызывается после закрытия диалогового окна отпечатка пальца. Если учетные данные были успешно сгенерированы, элемент data ActivityResult будет содержать информацию об учетных данных.

Сначала нам нужно извлечь из data PublicKeyCredential. Данные Intent имеют дополнительное поле массива байтов с ключом Fido.FIDO2_KEY_CREDENTIAL_EXTRA . Вы можете использовать статический метод в PublicKeyCredential под названием deserializeFromBytes , чтобы превратить массив байтов в объект PublicKeyCredential .

Затем проверьте, является ли элемент response этого объекта учетных данных AuthenticationErrorResponse . Если да, то произошла ошибка при создании учетных данных; в противном случае мы можем отправить учетные данные на наш сервер.

Готовый метод будет выглядеть так:

private fun handleCreateCredentialResult(activityResult: ActivityResult) {
  val bytes = activityResult.data?.getByteArrayExtra(Fido.FIDO2_KEY_CREDENTIAL_EXTRA)
  when {
    activityResult.resultCode != Activity.RESULT_OK ->
      Toast.makeText(requireContext(), R.string.cancelled, Toast.LENGTH_LONG).show()
    bytes == null ->
      Toast.makeText(requireContext(), R.string.credential_error, Toast.LENGTH_LONG)
        .show()
    else -> {
      val credential = PublicKeyCredential.deserializeFromBytes(bytes)
      val response = credential.response
      if (response is AuthenticatorErrorResponse) {
        Toast.makeText(requireContext(), response.errorMessage, Toast.LENGTH_LONG)
          .show()
      } else {
        viewModel.registerResponse(credential)
      }
    }
  }
}

Вызовите API сервера: /auth/registerResponse

Откройте AuthRepository.kt и найдите TODO(4).

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

Объект PublicKeyCredential содержит информацию о вновь созданных учетных данных. Теперь мы хотим запомнить идентификатор нашего локального ключа, чтобы отличать его от других ключей, зарегистрированных на сервере. В объекте PublicKeyCredential возьмите его свойство rawId и поместите его в локальную строковую переменную с помощью toBase64 .

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

Наконец, мы можем сохранить результаты в нашем DataStore . Список учетных данных следует сохранить с ключом CREDENTIALS в виде StringSet . Вы можете использовать toStringSet для преобразования списка учетных данных в StringSet .

Кроме того, мы сохраняем идентификатор учетных данных с ключом LOCAL_CREDENTIAL_ID .

suspend fun registerResponse(credential: PublicKeyCredential) {
  try {
    val sessionId = dataStore.read(SESSION_ID)!!
    val credentialId = credential.rawId.toBase64()
    when (val result = api.registerResponse(sessionId, credential)) {
      ApiResult.SignedOutFromServer -> forceSignOut()
      is ApiResult.Success -> {
        dataStore.edit { prefs ->
          result.sessionId?.let { prefs[SESSION_ID] = it }
          prefs[CREDENTIALS] = result.data.toStringSet()
          prefs[LOCAL_CREDENTIAL_ID] = credentialId
        }
      }
    }
  } catch (e: ApiException) {
    Log.e(TAG, "Cannot call registerResponse", e)
  }
}

Запустите приложение, и вы сможете нажать на FAB и зарегистрировать новые учетные данные.

7d64d9289c5a3cbc.png

6. Аутентификация пользователя по отпечатку пальца

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

Вызовите API сервера: /auth/signinRequest

Откройте AuthRepository.kt и найдите TODO(5).

Этот метод signinRequest вызывается при открытии AuthFragment . Здесь мы хотим запросить сервер и посмотреть, сможем ли мы разрешить пользователю войти в систему с помощью FIDO2.

Сначала нам нужно получить PublicKeyCredentialRequestOptions с сервера. Используйте api.signInRequest для вызова API сервера. Возвращенный ApiResult содержит PublicKeyCredentialRequestOptions .

С помощью PublicKeyCredentialRequestOptions мы можем использовать API FIDO2 getSignIntent для создания PendingIntent для открытия диалогового окна отпечатка пальца.

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

suspend fun signinRequest(): PendingIntent? {
  fido2ApiClient?.let { client ->
    val sessionId = dataStore.read(SESSION_ID)!!
    val credentialId = dataStore.read(LOCAL_CREDENTIAL_ID)
    if (credentialId != null) {
      when (val apiResult = api.signinRequest(sessionId, credentialId)) {
        ApiResult.SignedOutFromServer -> forceSignOut()
        is ApiResult.Success -> {
          val task = client.getSignPendingIntent(apiResult.data)
          return task.await()
        }
      }
    }
  }
  return null
}

Откройте диалоговое окно отпечатка пальца для подтверждения.

Откройте AuthFragment.kt и найдите TODO(6).

Это почти то же самое, что мы делали при регистрации. Мы можем запустить диалоговое окно отпечатка пальца с помощью члена signIntentLauncher .

viewLifecycleOwner.lifecycleScope.launchWhenStarted {
  launch {
    viewModel.signinRequests.collect { intent ->
      signIntentLauncher.launch(
        IntentSenderRequest.Builder(intent).build()
      )
    }
  }
  launch {
    ...
  }
}

Обработка результата активности

Откройте AuthFragment.kt и найдите TODO(7).

Опять же, это то же самое, что мы делали при регистрации. Мы можем извлечь PublicKeyCredential , проверить наличие ошибки и передать его в ViewModel.

private fun handleSignResult(activityResult: ActivityResult) {
  val bytes = activityResult.data?.getByteArrayExtra(Fido.FIDO2_KEY_CREDENTIAL_EXTRA)
  when {
    activityResult.resultCode != Activity.RESULT_OK ->
      Toast.makeText(requireContext(), R.string.cancelled, Toast.LENGTH_SHORT).show()
    bytes == null ->
      Toast.makeText(requireContext(), R.string.auth_error, Toast.LENGTH_SHORT)
        .show()
    else -> {
      val credential = PublicKeyCredential.deserializeFromBytes(bytes)
      val response = credential.response
      if (response is AuthenticatorErrorResponse) {
        Toast.makeText(requireContext(), response.errorMessage, Toast.LENGTH_SHORT)
          .show()
      } else {
        viewModel.signinResponse(credential)
      }
    }
  }
}

Вызовите API сервера: /auth/signinResponse

Откройте AuthRepository.kt и найдите TODO(8).

Объект PublicKeyCredential имеет идентификатор учетных данных как keyHandle . Как и в процессе регистрации, давайте сохраним это в локальной строковой переменной, чтобы иметь возможность сохранить ее позже.

Теперь мы готовы вызвать API сервера с помощью api.signinResponse . Возвращаемое значение содержит список учетных данных.

На этом этапе вход успешен. Нам нужно сохранить все результаты в нашем DataStore . Список учетных данных должен храниться как StringSet с ключом CREDENTIALS . Локальный идентификатор учетных данных, который мы сохранили выше, должен храниться в виде строки с ключом LOCAL_CREDENTIAL_ID .

Наконец, нам нужно обновить состояние входа, чтобы пользовательский интерфейс мог перенаправлять пользователя на HomeFragment. Это можно сделать, отправив объект SignInState.SignedIn в SharedFlow с именем signInStateMutable . Мы также хотим вызвать refreshCredentials , чтобы получить учетные данные пользователя, чтобы они были указаны в пользовательском интерфейсе.

suspend fun signinResponse(credential: PublicKeyCredential) {
  try {
    val username = dataStore.read(USERNAME)!!
    val sessionId = dataStore.read(SESSION_ID)!!
    val credentialId = credential.rawId.toBase64()
    when (val result = api.signinResponse(sessionId, credential)) {
      ApiResult.SignedOutFromServer -> forceSignOut()
      is ApiResult.Success -> {
        dataStore.edit { prefs ->
          result.sessionId?.let { prefs[SESSION_ID] = it }
          prefs[CREDENTIALS] = result.data.toStringSet()
          prefs[LOCAL_CREDENTIAL_ID] = credentialId
        }
        signInStateMutable.emit(SignInState.SignedIn(username))
        refreshCredentials()
      }
    }
  } catch (e: ApiException) {
    Log.e(TAG, "Cannot call registerResponse", e)
  }
}

Запустите приложение и нажмите «Reauth», чтобы открыть AuthFragment . Теперь вы должны увидеть диалоговое окно с отпечатком пальца, предлагающее войти в систему с помощью отпечатка пальца.

45f81419f84952c8.png

Поздравляю! Теперь вы узнали, как использовать API FIDO2 на Android для регистрации и входа в систему.

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

Вы успешно завершили работу над кодом — ваш первый Android FIDO2 API .

Что вы узнали

  • Как зарегистрировать учетные данные с помощью аутентификатора платформы, проверяющего пользователя.
  • Как аутентифицировать пользователя с помощью зарегистрированного аутентификатора.
  • Доступные варианты регистрации нового аутентификатора.
  • Лучшие UX-практики повторной аутентификации с использованием биометрического датчика.

Следующий шаг

  • Узнайте, как создать аналогичный опыт на веб-сайте.

Вы можете научиться этому, попробовав кодовую лабораторию Your first WebAuthn !

Ресурсы

Особая благодарность Юрию Акерманну из FIDO Alliance за помощь.