Как реализовать восстановление учетных данных с помощью Менеджера учетных данных

На этой странице описано, как создать ключ восстановления, войти с его помощью и удалить его.

Совместимость версий

Функция восстановления учетных данных в Менеджере учетных данных работает на устройствах с Android 9 (API уровня 28) и более поздними версиями, сервисами Google Play (GMS) Core версии 242200000 (v24.22) и более поздними, а также библиотекой androidx.credentials версии 1.5.0 и более поздними. На устройствах без сервисов Google Play или со старой версией GMS перехватывайте CreateCredentialProviderConfigurationException и GetCredentialProviderConfigurationException, чтобы приложение могло корректно вернуться к стандартному входу.

Требования

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

Связанные запросы

Добавьте в файл build.gradle модуля приложения следующие зависимости:

Котлин

dependencies {
    implementation("androidx.credentials:credentials:1.7.0-alpha03")
    implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha03")
}

Круто

dependencies {
    implementation "androidx.credentials:credentials:1.7.0-alpha03"
    implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha03"
}

Восстановление учетных данных доступно в библиотеке androidx.credentials версии 1.5.0 и более поздних. Однако по возможности рекомендуется использовать последние стабильные версии зависимостей.

Как android:allowBackup влияет на восстановление учетных данных

Атрибут манифеста android:allowBackup управляет резервным копированием данных приложения и взаимодействует с Restore Credentials API тремя способами:

  • Перенос ключа восстановления (не зависит от allowBackup). Сервисы Google Play переносят пару ключей RestoreCredential, используя сквозное шифрование облачной резервной копии или локальный перенос с устройства на устройство, независимо от того, какое значение имеет android:allowBackup: true или false. При переносе с устройства на устройство ключ восстановления переносится всегда. Он не переносится только при резервном копировании в облако, если пользователь отключил эту функцию в настройках Android.
  • Фоновое восстановление в BackupAgent (требуется android:allowBackup="true"). Фоновый вход в аккаунт до того, как пользователь откроет приложение на новом устройстве (BackupAgent.onRestoreFinished()), выполняется, только если в манифесте настроены android:allowBackup="true" и android:backupAgent. Если android:allowBackup="false", восстановите сеанс, когда пользователь впервые запустит основное Activity. Вы также можете реализовать оба подхода, чтобы сделать восстановление более надежным.
  • Тестирование резервного копирования в облако в Android Studio (требуется android:allowBackup="true"). Чтобы протестировать резервное копирование в облако с помощью инструмента резервного копирования и восстановления в Android Studio, требуется android:allowBackup="true". Тестирование переноса данных с устройства на устройство работает независимо от allowBackup.

Обзор

Жизненный цикл восстановления учетных данных состоит из трех этапов, которые выполняются на старом и новом устройствах пользователя:

  1. Создайте ключ восстановления на предыдущем устройстве. После того как пользователь войдет в аккаунт (или при запуске приложения, если пользователь уже вошел в аккаунт и в локальном хранилище нет ключа восстановления), получите параметры создания с сервера проверяющей стороны и вызовите credentialManager.createCredential().
  2. Выполните вход с помощью ключа восстановления на новом устройстве. Получите перенесенный RestoreCredential, используя двухуровневую стратегию. Сначала выполните вход автоматически в BackupAgent.onRestoreFinished() (когда android:allowBackup="true"), чтобы сразу возобновить фоновую синхронизацию и push-уведомления. Затем используйте резервный вариант в средстве запуска Activity.onCreate() при первом запуске приложения.
  3. Удаляйте ключ восстановления при выходе из аккаунта. Когда пользователь выходит из аккаунта или сервер признает сеанс недействительным, вызовите credentialManager.clearCredentialState() с ClearCredentialStateRequest(ClearCredentialStateRequest.TYPE_CLEAR_RESTORE_CREDENTIAL) и очистите локальный флаг синхронизации.

Создайте ключ восстановления

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

  • Если пользователь вошел в аккаунт и ключ восстановления ещё не создан (например, в методе onCreate() для основного Activity).
  • Когда пользователь входит в аккаунт или регистрирует новый.

Чтобы не вызывать createCredential() при каждом запуске приложения, сохраните в локальном хранилище флаг синхронизации, например has_synced_restore_credential. Не храните этот флаг в файле SharedPreferences или DataStore, для которого по умолчанию выполняется резервное копирование, если этот файл не исключен в data_extraction_rules.xml. В противном случае функция автоматического копирования в облако перенесет has_synced_restore_credential = true на новое устройство и не позволит ему создать или обновить собственный ключ восстановления.

Создайте экземпляр Менеджера учетных данных

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

// Use your app or activity context to instantiate a client instance of
// CredentialManager.
private val credentialManager = CredentialManager.create(context)

Получите параметры создания учетных данных от сервера приложения.

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

JSON-файл с параметрами создания имеет ту же структуру, что и регистрация ключа доступа:

{
  "challenge": "<base64url-encoded challenge>",
  "rp": {
    "name": "<relying party name>",
    "id": "<relying party host name>"
  },
  "user": {
    "id": "<base64url-encoded user ID>",
    "name": "<user name>",
    "displayName": "<user display name>"
  },
  "pubKeyCredParams": [
    {
      "type": "public-key",
      "alg": -7
    }
  ],
  "attestation": "none",
  "excludeCredentials": [
    {
        "id": "<base64url-encoded credential ID to exclude>", 
        "type": "public-key"
    }
  ],
  "authenticatorSelection": {
    "requireResidentKey": true,
    "residentKey": "required",
    "userVerification": "required"
  }
}

Убедитесь, что JSON-код соответствует следующим требованиям:

  • challenge и user.id. Закодируйте оба значения как строки Base64URL без заполнения (замените + на -, / на _ и удалите конечные символы =). Стандартные символы Base64 или необработанные целочисленные идентификаторы вызывают ошибку CreateRestoreCredentialDomException или IllegalArgumentException.
  • rp.id: укажите домен проверяющей стороны (например, example.com).
  • pubKeyCredParams: укажите {"type": "public-key", "alg": -7} (ES256) и {"type": "public-key", "alg": -257} (RS256).

Создайте ключ восстановления

Получив от сервера JSON с параметрами создания открытого ключа, упакуйте его в объект CreateRestoreCredentialRequest и вызовите createCredential() для объекта CredentialManager.

// requestJson contains the credential creation options sent by the server
val createRestoreRequest = CreateRestoreCredentialRequest(requestJson)
val response = try {
    credentialManager.createCredential(context, createRestoreRequest)
} catch (e: E2eeUnavailableException) {
    // Fall back to local device-to-device (D2D) restore if cloud E2EE is unavailable
    credentialManager.createCredential(
        context,
        CreateRestoreCredentialRequest(requestJson, isCloudBackupEnabled = false),
    )
}

Важные сведения о коде

  • Объект CreateRestoreCredentialRequest содержит следующие поля:

    • requestJson – параметры создания учетных данных, отправленные сервером приложения в формате Web Authentication API для PublicKeyCredentialCreationOptionsJSON.
    • isCloudBackupEnabled: поле Boolean, определяющее, нужно ли сохранять ключ восстановления в облаке. По умолчанию для этого параметра задано значение true. Это поле может принимать следующие значения:

      • true: (рекомендуется) это значение позволяет создавать резервные копии ключей восстановления в облаке, если у пользователя включены резервное копирование Google и сквозное шифрование, например блокировка экрана.
      • false – ключ сохраняется локально, а не в облаке. Ключ не будет доступен на новом устройстве, если пользователь решит восстановить данные из облака, но он все равно будет передан при локальной настройке с помощью кабеля или Wi-Fi.

Обработка ответа на создание учетных данных

Credential Manager API возвращает ответ типа CreateRestoreCredentialResponse. Этот ответ содержит ответ на регистрацию учетных данных открытого ключа в формате JSON.

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

При создании ключа восстановления обработайте следующие исключения:

  • CreateRestoreCredentialDomException – это исключение возникает, если значение requestJson недействительно и не соответствует формату WebAuthn для PublicKeyCredentialCreationOptionsJSON.
  • E2eeUnavailableException. Эта ошибка возникает, если для параметра isCloudBackupEnabled задано значение true, но на устройстве пользователя не настроено резервное копирование данных или сквозное шифрование, например блокировка экрана.
    Чтобы учетные данные для восстановления создавались для локального переноса с устройства на устройство, даже если сквозное шифрование в облаке недоступно, перехватите E2eeUnavailableException и сразу же повторите попытку createCredential() с CreateRestoreCredentialRequest(requestJson, isCloudBackupEnabled = false).
  • IllegalArgumentException. Эта ошибка возникает, если requestJson не заполнено, не является допустимым JSON или не имеет допустимого user.id в кодировке Base64URL, соответствующего спецификациям WebAuthn.

Как войти с помощью ключа восстановления

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

Получите параметры извлечения учетных данных с сервера приложения

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

JSON-файл с настройками извлечения имеет следующую структуру:

{
  "challenge": "<your app challenge>",
  "allowCredentials": [],
  "rpId": "<your app server domain>"
}

Настройте параметры извлечения JSON на сервере следующим образом:

  • allowCredentials – пропустите это поле или передайте пустой массив ([]), поскольку аккаунт пользователя неизвестен до восстановления. После того как getCredential() вернет ответ, ваш сервер идентифицирует пользователя по полю response.userHandle (которое соответствует user.id, зарегистрированному при создании) и найдет открытый ключ по id (credentialId).
  • userVerification: задайте значение "discouraged" (или "preferred"). Поскольку Restore Credentials выполняется в фоновом режиме без интерфейса биометрической аутентификации или блокировки экрана, Android переопределяет "required" на "discouraged", а в данных аутентификатора, возвращаемых на ваш сервер, для флага User Verified (uv) задано значение false. Настройте валидатор сервера WebAuthn, чтобы он принимал uv = false для утверждений ключа восстановления.

Получите ключ восстановления

Чтобы получить ключ восстановления на новом устройстве, упакуйте requestJson сервера в GetRestoreCredentialOption внутри GetCredentialRequest и вызовите getCredential() для объекта CredentialManager.

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

  • Уровень 1 (фоновое восстановление в BackupAgent.onRestoreFinished()). Если резервное копирование данных приложения включено (android:allowBackup="true"), создайте подкласс BackupAgent и переопределите onRestoreFinished() (не используйте onRestore(), который вызывается только для устаревших резервных копий типа "ключ-значение"). Поскольку onRestoreFinished() – синхронный метод, а getCredential() – функция suspend Kotlin, вызов нужно связать с помощью runBlocking(Dispatchers.IO). Перехватывайте NoCredentialException, когда ключ восстановления отсутствует.
  • Уровень 2 (резервный вариант на переднем плане в средстве запуска Activity.onCreate()). При первом запуске приложения, когда локальный сеанс отсутствует, вызовите getCredential() из lifecycleScope, прежде чем показывать экран входа. Это позволяет обрабатывать приложения с android:allowBackup="false" и восстанавливать данные, если устройство было офлайн во время выполнения onRestoreFinished().

Если вы добавляете новый BackupAgent в приложение, которое уже использует автоматическое копирование в облако с android:allowBackup="true", также установите android:fullBackupOnly="true" для элемента <application> в AndroidManifest.xml, чтобы Android продолжал использовать автоматическое копирование в облако вместо перехода на устаревшее резервное копирование типа "ключ-значение".

    suspend fun getRestoreKey(
        fetchAuthenticationJson: () -> String,
    ) {
        // Fetch the options required to get the restore key
        val authenticationJson = fetchAuthenticationJson()

        // Create the GetRestoreCredentialOption object
        val options = GetRestoreCredentialOption(authenticationJson)
        val getRequest = GetCredentialRequest(listOf(options))

        try {
            val response = credentialManager.getCredential(context, getRequest)

            // Type-check and extract the restore credential
            val credential = response.credential
            if (credential is RestoreCredential) {
                val responseJson = credential.authenticationResponseJson
            }
        } catch (e: NoCredentialException) {
            // No restore key is present on this device
        }
    }

class MyBackupAgent : BackupAgent() {

    override fun onRestoreFinished() {
        super.onRestoreFinished()
        val credentialManager = CredentialManager.create(this)
        val restoreFunctions = RestoreCredentialsFunctions(this, credentialManager)

        runBlocking(Dispatchers.IO) {
            restoreFunctions.getRestoreKey(
                fetchAuthenticationJson = {
                    /* Fetch challenge JSON from server */ ""
                }
            )
        }
    }
}

Credential Manager API возвращает объект GetCredentialResponse, свойство credential которого является объектом RestoreCredential (не PublicKeyCredential), содержащим authenticationResponseJson.

Обработка ответа на запрос входа

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

При разработке серверного хранилища и правил жизненного цикла для ключей восстановления следуйте этим рекомендациям:

  • Отличайте ключи восстановления от ключей доступа пользователей. Помечайте ключи восстановления в базе данных, чтобы они не показывались на пользовательских экранах управления ключами доступа и не требовали подтверждения пользователя (uv = true).
  • Поддержка нескольких активных устройств для одного пользователя. У пользователя может быть несколько устройств, например телефон и планшет. Разрешите несколько активных ключей восстановления для каждого аккаунта пользователя (например, по идентификатору устройства или установки), а не перезаписывайте одну строку.
  • Установите подходящий TTL и удалите неиспользуемые ключи. При удалении приложения или данных приложения локальный ключ восстановления удаляется без уведомления сервера. Храните ключи восстановления достаточно долго, чтобы они оставались в силе во время обновления устройства (даже если пользователь вышел из аккаунта на старом устройстве после переноса данных). Удаляйте неактивные ключи по истечении срока хранения (TTL) или при достижении лимита на количество ключей для одного пользователя.

Удалить ключ восстановления

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

Ключи восстановления удаляются только в следующих случаях:

  • Действия на уровне системы. Пользователь удаляет приложение или его данные.
  • Вызовы на уровне приложения. Удалите ключ программно, вызвав clearCredentialState() при обработке выхода пользователя из аккаунта в коде приложения.

Поскольку стандартная команда ClearCredentialStateRequest() очищает только состояние стандартного сеанса и не затрагивает ключ восстановления, при выходе из аккаунта выполните двойную очистку:

  1. Вызовите credentialManager.clearCredentialState(ClearCredentialStateRequest()), чтобы очистить состояние сеанса активного поставщика учетных данных.
  2. Вызовите credentialManager.clearCredentialState() с ClearCredentialStateRequest(ClearCredentialStateRequest.TYPE_CLEAR_RESTORE_CREDENTIAL), чтобы удалить сохраненный RestoreCredential.
  3. Удалите локальный маркер has_synced_restore_credential из локального хранилища.
// Clear active credential provider session state
credentialManager.clearCredentialState(ClearCredentialStateRequest())

// When the user logs out, delete the restore key
val clearRequest = ClearCredentialStateRequest(TYPE_CLEAR_RESTORE_CREDENTIAL)
val response = credentialManager.clearCredentialState(clearRequest)

Дальнейшие действия