Implementowanie przywracania danych logowania za pomocą Menedżera danych logowania

Na tej stronie dowiesz się, jak utworzyć klucz przywracania, jak się za jego pomocą zalogować i jak go usunąć.

Zgodność wersji

Funkcja przywracania danych logowania w Menedżerze danych logowania działa na urządzeniach z Androidem 9 (poziom interfejsu API 28) lub nowszym, Usługami Google Play (GMS) w wersji 242200000 (24.22) lub nowszej oraz biblioteką androidx.credentials w wersji 1.5.0 lub nowszej. Na urządzeniach bez usług Google Play lub ze starszą wersją GMS przechwyć CreateCredentialProviderConfigurationException i GetCredentialProviderConfigurationException, aby aplikacja mogła płynnie przejść do standardowego logowania.

Wymagania wstępne

Skonfiguruj serwer strony zależnej WebAuthn podobny do serwera kluczy dostępu. Jeśli masz już skonfigurowany serwer do obsługi uwierzytelniania za pomocą kluczy dostępu, możesz ponownie użyć tych samych punktów końcowych rejestracji i asercji WebAuthn do kluczy przywracania.

Zależności

Dodaj do pliku build.gradle modułu aplikacji te zależności:

Kotlin

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

Groovy

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

Interfejs Restore Credentials API jest dostępny w bibliotece androidx.credentials w wersji 1.5.0 i nowszych. Zalecamy jednak używanie najnowszych stabilnych wersji zależności, jeśli to możliwe.

Jak android:allowBackup wpływa na przywracanie danych logowania

Atrybut manifestu android:allowBackup kontroluje tworzenie kopii zapasowej danych aplikacji, która współdziała z interfejsem Restore Credentials na 3 różne sposoby:

  • Przywracanie przenoszenia kluczy (niezależnie od allowBackup): Usługi Google Play przenoszą parę kluczy RestoreCredential za pomocą w pełni zaszyfrowanej kopii zapasowej w chmurze lub lokalnego przenoszenia z urządzenia na urządzenie, niezależnie od tego, czy android:allowBackup to true czy false. W przypadku przenoszenia danych między urządzeniami klucz przywracania jest zawsze przenoszony. Jedynym wyjątkiem jest sytuacja, gdy użytkownik wyłączył kopię zapasową w chmurze w ustawieniach Androida.
  • Przywracanie w tle w BackupAgent (wymaga android:allowBackup="true"): ciche logowanie w tle, zanim użytkownik otworzy aplikację na nowym urządzeniu (BackupAgent.onRestoreFinished()), działa tylko wtedy, gdy w pliku manifestu skonfigurowano android:allowBackup="true" i android:backupAgent. Jeśli android:allowBackup="false", przywróć sesję, gdy użytkownik po raz pierwszy uruchomi główną Activity. Możesz też wdrożyć oba podejścia, aby zwiększyć niezawodność przywracania.
  • Testowanie kopii zapasowej w chmurze w Android Studio (wymaga android:allowBackup="true"): testowanie typu kopii zapasowej w chmurze w narzędziu do tworzenia i przywracania kopii zapasowej w Android Studio wymaga android:allowBackup="true", natomiast testowanie między urządzeniami działa niezależnie od allowBackup.

Przegląd

Przywracanie danych uwierzytelniających działa w 3 etapach cyklu życia na poprzednim i nowym urządzeniu użytkownika:

  1. Utworzenie klucza przywracania na poprzednim urządzeniu: po zalogowaniu się użytkownika (lub przy uruchamianiu aplikacji, jeśli użytkownik jest już zalogowany i w pamięci lokalnej nie ma zapisanego klucza przywracania) pobierz opcje tworzenia z serwera strony zależnej i wywołaj credentialManager.createCredential().
  2. Logowanie się za pomocą klucza przywracania na nowym urządzeniu: pobierz przeniesiony klucz RestoreCredential, stosując strategię dwupoziomową: najpierw w sposób niezauważalny dla użytkownika w BackupAgent.onRestoreFinished() (gdy android:allowBackup="true"), aby natychmiast wznowić synchronizację w tle i powiadomienia push, a następnie jako rozwiązanie zastępcze w aktywności menu z aplikacjami Activity.onCreate() przy pierwszym uruchomieniu aplikacji.
  3. Usuwanie klucza przywracania po wylogowaniu: gdy użytkownik się wyloguje lub jego sesja zostanie unieważniona przez serwer, wywołaj credentialManager.clearCredentialState() z ClearCredentialStateRequest(ClearCredentialStateRequest.TYPE_CLEAR_RESTORE_CREDENTIAL) i wyczyść lokalną flagę synchronizacji.

Tworzenie klucza przywracania

Aplikacja powinna uwzględniać wszystkie przypadki logowania się użytkownika, aby mieć pewność, że aktywni użytkownicy mają utworzony klucz przywracania. Utwórz klucz przywracania w tych sytuacjach:

  • Jeśli użytkownik jest zalogowany, a klucz przywracania nie został jeszcze utworzony (np. w metodzie onCreate() dla głównego Activity).
  • gdy użytkownik loguje się lub kończy proces rejestracji nowego konta;

Aby uniknąć wywoływania metody createCredential() przy każdym uruchomieniu aplikacji, zapisz w pamięci lokalnej flagę synchronizacji lokalnej, np. has_synced_restore_credential. Nie przechowuj tej flagi w domyślnej kopii zapasowej SharedPreferences ani DataStore, chyba że plik jest wykluczony w data_extraction_rules.xml. W przeciwnym razie automatyczna kopia zapasowa skopiuje has_synced_restore_credential = true na nowe urządzenie i uniemożliwi mu wygenerowanie lub rotację własnego klucza przywracania.

Tworzenie instancji Menedżera danych logowania

Użyj kontekstu aktywności aplikacji, aby utworzyć instancję obiektu CredentialManager.

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

Pobieranie opcji tworzenia danych logowania z serwera aplikacji

Użyj biblioteki zgodnej ze standardem FIDO na serwerze aplikacji, aby wysłać do aplikacji klienckiej kod JSON PublicKeyCredentialCreationOptions wymagany do utworzenia danych logowania do przywracania. Więcej informacji o implementacji po stronie serwera znajdziesz w wytycznych dotyczących strony serwera.

Opcje tworzenia JSON mają taką samą strukturę jak rejestracja klucza dostępu:

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

Upewnij się, że kod JSON opcji tworzenia spełnia te wymagania:

  • challenge i user.id: zakoduj obie wartości jako ciągi Base64URL bez dopełnienia (zastępując + znakiem -, / znakiem _ i usuwając końcowe znaki =). Standardowe znaki Base64 lub nieprzetworzone identyfikatory liczb całkowitych powodują błąd CreateRestoreCredentialDomException lub IllegalArgumentException.
  • rp.id: ustaw domenę strony uzależnionej (na przykład example.com).
  • pubKeyCredParams: uwzględnij {"type": "public-key", "alg": -7} (ES256) i {"type": "public-key", "alg": -257} (RS256).

Tworzenie klucza przywracania

Po otrzymaniu z serwera pliku JSON z opcjami tworzenia klucza publicznego umieść go w obiekcie CreateRestoreCredentialRequest i wywołaj metodę createCredential() na obiekcie 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),
    )
}

Najważniejsze informacje o kodzie

  • Obiekt CreateRestoreCredentialRequest zawiera te pola:

    • requestJson: opcje tworzenia danych logowania wysyłane przez serwer aplikacji w formacie Web Authentication API dla PublicKeyCredentialCreationOptionsJSON.
    • isCloudBackupEnabled: pole Boolean, które określa, czy klucz przywracania powinien być zapisywany w kopii zapasowej w chmurze. Domyślnie ta flaga ma wartość true. To pole ma te wartości:

      • true: (zalecane) ta wartość umożliwia tworzenie kopii zapasowej kluczy przywracania w chmurze, jeśli użytkownik ma włączoną kopię zapasową Google i pełne szyfrowanie, np. blokadę ekranu.
      • false: ta wartość powoduje zapisanie klucza lokalnie, a nie w chmurze. Klucz nie jest dostępny na nowym urządzeniu, jeśli użytkownik zdecyduje się na przywrócenie danych z chmury, ale nadal jest przenoszony podczas lokalnej konfiguracji urządzenia z użyciem kabla lub Wi-Fi.

Obsługa odpowiedzi na żądanie utworzenia danych logowania

Interfejs Credential Manager API zwraca odpowiedź typu CreateRestoreCredentialResponse. Ta odpowiedź zawiera odpowiedź na rejestrację danych logowania klucza publicznego w formacie JSON.

Wyślij response.registrationResponseJson z aplikacji na serwer strony zależnej, aby zweryfikować i zapisać klucz publiczny na koncie użytkownika, a następnie zapisz lokalny znacznik synchronizacji w pamięci lokalnej. Więcej informacji o weryfikacji po stronie serwera znajdziesz we wskazówkach dotyczących kluczy dostępu.

Podczas procesu tworzenia klucza przywracania obsługuj te wyjątki:

  • CreateRestoreCredentialDomException: ten wyjątek występuje, jeśli parametr requestJson jest nieprawidłowy i nie jest zgodny z formatem WebAuthn dla PublicKeyCredentialCreationOptionsJSON.
  • E2eeUnavailableException: ten wyjątek występuje, gdy isCloudBackupEnabled ma wartość true, ale urządzenie użytkownika nie ma kopii zapasowej danych ani pełnego szyfrowania, takiego jak blokada ekranu.
    Aby mieć pewność, że dane uwierzytelniające do przywracania zostaną utworzone na potrzeby lokalnego przenoszenia danych między urządzeniami, nawet gdy pełne szyfrowanie w chmurze jest niedostępne, przechwyć E2eeUnavailableException i natychmiast ponów próbę createCredential() z CreateRestoreCredentialRequest(requestJson, isCloudBackupEnabled = false).
  • IllegalArgumentException: ten wyjątek występuje, jeśli parametr requestJson jest pusty, nie jest prawidłowym plikiem JSON lub nie ma prawidłowego parametru user.id zakodowanego w formacie Base64URL, który jest zgodny ze specyfikacjami WebAuthn.

Logowanie się za pomocą klucza przywracania

Użyj interfejsu Restore Credentials API, aby w sposób niezauważalny dla użytkownika zalogować go podczas procesu konfiguracji urządzenia.

Pobieranie opcji pobierania danych logowania z serwera aplikacji

Zanim wywołasz getCredential() na nowym urządzeniu, poproś serwer aplikacji o wyzwanie JSON WebAuthn PublicKeyCredentialRequestOptions. Ponieważ aplikacja na nowym urządzeniu nie wie jeszcze, który użytkownik się loguje, ten punkt końcowy musi być nieuwierzytelniony (wykrywanie bez użytkownika). Wskazówki dotyczące serwera znajdziesz w artykule Logowanie się za pomocą klucza dostępu i przewodniku po uwierzytelnianiu po stronie serwera.

Plik JSON z opcjami pobierania ma taką strukturę:

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

Skonfiguruj plik JSON z opcjami pobierania na serwerze w ten sposób:

  • allowCredentials: pomiń to pole lub przekaż pustą tablicę ([]), ponieważ konto użytkownika nie jest znane przed przywróceniem. Gdy getCredential() zwróci dane, Twój serwer zidentyfikuje użytkownika na podstawie pola response.userHandle (które odpowiada wartości user.id zarejestrowanej podczas tworzenia) i wyszuka klucz publiczny według id (credentialId).
  • userVerification: ustaw na "discouraged" (lub "preferred"). Ponieważ funkcja przywracania danych logowania działa w tle bez interfejsu biometrycznego lub blokady ekranu, Android zastępuje "required" wartością "discouraged", a dane uwierzytelniające zwracane na serwer mają flagę User Verified (uv) ustawioną na false. Skonfiguruj weryfikator serwera WebAuthn, aby akceptował uv = false w przypadku asercji klucza przywracania.

Uzyskiwanie klucza przywracania

Aby uzyskać klucz przywracania na nowym urządzeniu, opakuj requestJson serwera w GetRestoreCredentialOption wewnątrz GetCredentialRequest i wywołaj getCredential() na obiekcie CredentialManager.

Wdróż dwupoziomową strategię przywracania, aby użytkownicy byli zalogowani niezależnie od tego, czy przywracanie danych aplikacji w tle jest uruchomione:

  • Poziom 1 (przywracanie w tle w BackupAgent.onRestoreFinished()): jeśli kopia zapasowa danych aplikacji jest włączona (android:allowBackup="true"), utwórz podklasę BackupAgent i zastąp onRestoreFinished() (nie używaj onRestore(), która jest wywoływana tylko w przypadku starszych kopii zapasowych klucz-wartość). Ponieważ onRestoreFinished() jest synchroniczna, a getCredential() to funkcja suspend w Kotlinie, połącz wywołanie za pomocą runBlocking(Dispatchers.IO). Ciche przechwytywanie wyjątku NoCredentialException, gdy nie ma klucza przywracania.
  • Poziom 2 (rezerwowa usługa działająca na pierwszym planie w programie uruchamiającym Activity.onCreate()): przy pierwszym uruchomieniu aplikacji, gdy nie ma sesji lokalnej, wywołaj getCredential() z lifecycleScope przed wyświetleniem ekranu logowania. Dotyczy to aplikacji z android:allowBackup="false" i przywraca stan, jeśli urządzenie było offline, gdy uruchomiono onRestoreFinished().

Jeśli dodasz nowy element BackupAgent do aplikacji, która już korzysta z automatycznej kopii zapasowej z android:allowBackup="true", ustaw też android:fullBackupOnly="true" w elemencie <application> w AndroidManifest.xml, aby Android nadal korzystał z automatycznej kopii zapasowej zamiast przełączać się na starszą kopię zapasową klucz-wartość.

    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 */ ""
                }
            )
        }
    }
}

Interfejs Credential Manager API zwraca obiekt GetCredentialResponse, którego właściwość credential to RestoreCredential (a nie PublicKeyCredential) zawierający authenticationResponseJson.

Obsługa odpowiedzi na żądanie logowania

Wyślij restoreCredential.authenticationResponseJson z aplikacji na serwer strony uzależnionej, aby zweryfikować asercję kryptograficzną i wygenerować nowy token sesji dla użytkownika. Szczegółowe informacje o weryfikacji po stronie serwera znajdziesz w artykule Logowanie się za pomocą klucza dostępu.

Podczas projektowania pamięci masowej backendu i zasad cyklu życia kluczy przywracania postępuj zgodnie z tymi wytycznymi:

  • Odróżnianie kluczy przywracania od kluczy dostępu użytkownika: oznacz klucze przywracania w bazie danych, aby były ukryte na ekranach zarządzania kluczami dostępu dla użytkowników i nie były objęte weryfikacją użytkownika (uv = true).
  • Obsługa wielu aktywnych urządzeń na użytkownika: użytkownik może mieć kilka urządzeń (np. telefon i tablet). Zezwól na wiele aktywnych kluczy przywracania na konto użytkownika (np. klucze według urządzenia lub identyfikatora instalacji) zamiast zastępować pojedynczy wiersz.
  • Ustaw odpowiedni czas TTL i usuń osierocone klucze: odinstalowanie aplikacji lub wyczyszczenie danych aplikacji powoduje usunięcie lokalnego klucza przywracania bez powiadamiania serwera. Przechowuj klucze przywracania wystarczająco długo, aby przetrwały okna uaktualnienia urządzenia (nawet jeśli użytkownik wyloguje się na starym urządzeniu po przeniesieniu), i usuwaj nieaktualne klucze na podstawie czasu TTL braku aktywności lub limitów kluczy na użytkownika.

Usuń klucz przywracania

Credential Manager jest bezstanowy i nie ma informacji o aktywności użytkownika, więc nie usuwa automatycznie kluczy przywracania po ich użyciu. Aby usunąć klucz przywracania, wywołaj metodę clearCredentialState(). Ze względów bezpieczeństwa usuwaj klucz za każdym razem, gdy użytkownik się wyloguje, aby nie został on automatycznie zalogowany przy następnym uruchomieniu aplikacji.

Klucze przywracania są usuwane tylko w tych sytuacjach:

  • Działania na poziomie systemu: użytkownicy odinstalowują aplikację lub czyszczą jej dane.
  • Wywołania na poziomie aplikacji: usuń klucz automatycznie, wywołując funkcję clearCredentialState() podczas obsługi wylogowywania użytkownika w kodzie aplikacji.

Domyślna wartość ClearCredentialStateRequest() powoduje wyczyszczenie tylko standardowego stanu sesji i pozostawia klucz przywracania bez zmian, dlatego przy wylogowywaniu wykonaj podwójne czyszczenie:

  1. Wywołaj credentialManager.clearCredentialState(ClearCredentialStateRequest()), aby wyczyścić stan aktywnej sesji dostawcy danych logowania.
  2. Wywołaj credentialManager.clearCredentialState() z ClearCredentialStateRequest(ClearCredentialStateRequest.TYPE_CLEAR_RESTORE_CREDENTIAL), aby usunąć przechowywane RestoreCredential.
  3. Usuń lokalny znacznik has_synced_restore_credential z pamięci lokalnej.
// 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)

Dalsze kroki