Migracja z FIDO2 do menedżera danych logowania

Dzięki obsłudze kluczy dostępu, logowania sfederowanego i dostawców uwierzytelniania innych firm Credential Manager jest zalecanym interfejsem API do uwierzytelniania na Androidzie, który zapewnia bezpieczne i wygodne środowisko umożliwiające użytkownikom synchronizowanie danych logowania i zarządzanie nimi. Deweloperzy, którzy używają lokalnych danych logowania FIDO2, powinni zaktualizować aplikację, aby obsługiwała uwierzytelnianie za pomocą klucza dostępu, integrując ją z interfejsem Credential Manager API. Z tego dokumentu dowiesz się, jak przenieść projekt z FIDO2 do Credential Manager.

Powody, dla których warto przejść z FIDO2 na Menedżera danych logowania

W większości przypadków dostawcę uwierzytelniania w aplikacji na Androida należy przenieść do usługi Credential Manager. Powody, dla których warto przejść na Credential Manager:

  • Obsługa kluczy dostępu: Menedżer danych logowania obsługuje klucze dostępu, czyli nowy mechanizm uwierzytelniania bez hasła, który jest bezpieczniejszy i łatwiejszy w użyciu niż hasła.
  • Wiele metod logowania: Menedżer danych logowania obsługuje wiele metod logowania, w tym hasła, klucze dostępu i metody logowania federacyjnego. Dzięki temu użytkownicy mogą łatwiej uwierzytelniać się w aplikacji, niezależnie od preferowanej metody uwierzytelniania.
  • Obsługa zewnętrznych dostawców danych logowania: na urządzeniach z Androidem 14 i nowszym Menedżer danych logowania obsługuje wielu zewnętrznych dostawców danych logowania. Oznacza to, że użytkownicy mogą używać dotychczasowych danych logowania od innych dostawców, aby logować się w Twojej aplikacji.
  • Spójne wrażenia użytkowników: Menedżer danych logowania zapewnia spójniejsze wrażenia użytkowników podczas uwierzytelniania w aplikacjach i mechanizmach logowania. Ułatwia to użytkownikom zrozumienie i korzystanie z procesu uwierzytelniania w aplikacji.

Aby rozpocząć migrację z FIDO2 do Credential Manager, wykonaj te czynności.

Aktualizowanie zależności

  1. Zaktualizuj wtyczkę Kotlin w pliku build.gradle projektu do wersji 1.8.10 lub nowszej.

      plugins {
        //…
          id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
        //…
      }
    
  2. W pliku build.gradle projektu zaktualizuj zależności, aby używać najnowszych wersji bibliotek Credential Manager i Usług Play do uwierzytelniania.

      dependencies {
        // ...
        // Credential Manager:
        implementation 'androidx.credentials:credentials:<latest-version>'
    
        // Play Services Authentication:
        // Optional - needed for credentials support from play services, for devices running
        // Android 13 and below:
        implementation 'androidx.credentials:credentials-play-services-auth:<latest-version>'
        // ...
      }
    
  3. Zastąp inicjowanie FIDO inicjowaniem Menedżera danych logowania. Dodaj tę deklarację do klasy, której używasz do tworzenia kluczy dostępu i metod logowania:

    val credMan = CredentialManager.create(context)
    

Tworzenie kluczy dostępu

Zanim użytkownik będzie mógł się zalogować za pomocą klucza dostępu, musisz utworzyć nowy klucz dostępu, powiązać go z kontem użytkownika i zapisać klucz publiczny klucza dostępu na serwerze. Aby skonfigurować aplikację z tą funkcją, zaktualizuj wywołania funkcji rejestru.

Rysunek 1. Ilustracja przedstawiająca wymianę danych między aplikacją a serwerem podczas tworzenia klucza dostępu za pomocą Menedżera danych logowania.
  1. Aby uzyskać niezbędne parametry, które są wysyłane do metody createCredential() podczas tworzenia klucza dostępu, dodaj name("residentKey").value("required") (zgodnie z opisem w specyfikacji WebAuthn) do wywołania serwera registerRequest().

    suspend fun registerRequest() {
        // ...
        val call = client.newCall(
            Builder()
                .method("POST", jsonRequestBody {
                    name("attestation").value("none")
                    name("authenticatorSelection").objectValue {
                        name("residentKey").value("required")
                    }
            }).build()
        )
        // ...
    }
    
  2. Ustaw typ return dla funkcji registerRequest() i wszystkich funkcji podrzędnych na JSONObject.

    suspend fun registerRequest(sessionId: String): ApiResult<JSONObject> {
        val call = client.newCall(
            Builder()
                .url("$BASE_URL/<your api url>")
                .addHeader("Cookie", formatCookie(sessionId))
                .method("POST", jsonRequestBody {
                    name("attestation").value("none")
                    name("authenticatorSelection").objectValue {
                        name("authenticatorAttachment").value("platform")
                        name("userVerification").value("required")
                        name("residentKey").value("required")
                    }
                }).build()
        )
        val response = call.await()
        return response.result("Error calling the api") {
            parsePublicKeyCredentialCreationOptions(
                body ?: throw ApiException("Empty response from the api call")
            )
        }
    }
    
  3. Bezpiecznie usuń z widoku wszystkie metody, które obsługują wywołania narzędzia do uruchamiania intencji i wyniku aktywności.

  4. Ponieważ funkcja registerRequest() zwraca teraz wartość JSONObject, nie musisz tworzyć elementu PendingIntent. Zastąp zwróconą intencję wartością JSONObject. Zaktualizuj wywołania narzędzia do uruchamiania intencji, aby wywoływać createCredential() z interfejsu Credential Manager API. Wywołaj metodę interfejsu API createCredential().

    suspend fun createPasskey(
        activity: Activity,
        requestResult: JSONObject
    ): CreatePublicKeyCredentialResponse? {
        val request = CreatePublicKeyCredentialRequest(requestResult.toString())
        var response: CreatePublicKeyCredentialResponse? = null
        try {
            response = credMan.createCredential(
                request = request as CreateCredentialRequest,
                context = activity
            ) as CreatePublicKeyCredentialResponse
        } catch (e: CreateCredentialException) {
    
            showErrorAlert(activity, e)
    
            return null
        }
        return response
    }
    
  5. Po nawiązaniu połączenia wyślij odpowiedź z powrotem na serwer. Żądanie i odpowiedź w przypadku tego wywołania są podobne do implementacji FIDO2, więc nie są wymagane żadne zmiany.

Uwierzytelnianie za pomocą kluczy dostępu

Po skonfigurowaniu tworzenia kluczy dostępu możesz skonfigurować aplikację tak, aby użytkownicy mogli logować się i uwierzytelniać za pomocą kluczy dostępu. Aby to zrobić, zaktualizuj kod uwierzytelniania, aby obsługiwał wyniki Menedżera danych logowania, i wdroż funkcję uwierzytelniania za pomocą kluczy dostępu.

Rysunek 2. proces uwierzytelniania za pomocą klucza dostępu w Menedżerze danych logowania.
  1. Wywołanie żądania logowania na serwerze w celu uzyskania niezbędnych informacji do wysłania do żądania getCredential() jest takie samo jak w przypadku implementacji FIDO2. Nie musisz wprowadzać żadnych zmian.
  2. Podobnie jak w przypadku wywołania żądania rejestracji zwrócona odpowiedź jest w formacie JSONObject.

    /**
     * @param sessionId The session ID to be used for the sign-in.
     * @param credentialId The credential ID of this device.
     * @return a JSON object.
     */
    suspend fun signinRequest(): ApiResult<JSONObject> {
        val call = client.newCall(Builder().url(buildString {
            append("$BASE_URL/signinRequest")
        }).method("POST", jsonRequestBody {})
            .build()
        )
        val response = call.await()
        return response.result("Error calling /signinRequest") {
            parsePublicKeyCredentialRequestOptions(
                body ?: throw ApiException("Empty response from /signinRequest")
            )
        }
    }
    
    /**
     * @param sessionId The session ID to be used for the sign-in.
     * @param response The JSONObject for signInResponse.
     * @param credentialId id/rawId.
     * @return A list of all the credentials registered on the server,
     * including the newly-registered one.
     */
    suspend fun signinResponse(
        sessionId: String, response: JSONObject, credentialId: String
    ): ApiResult<Unit> {
    
        val call = client.newCall(
            Builder().url("$BASE_URL/signinResponse")
                .addHeader("Cookie",formatCookie(sessionId))
                .method("POST", jsonRequestBody {
                    name("id").value(credentialId)
                    name("type").value(PUBLIC_KEY.toString())
                    name("rawId").value(credentialId)
                    name("response").objectValue {
                        name("clientDataJSON").value(
                            response.getString("clientDataJSON")
                        )
                        name("authenticatorData").value(
                            response.getString("authenticatorData")
                        )
                        name("signature").value(
                            response.getString("signature")
                        )
                        name("userHandle").value(
                            response.getString("userHandle")
                        )
                    }
                }).build()
        )
        val apiResponse = call.await()
        return apiResponse.result("Error calling /signingResponse") {
        }
    }
    
  3. Bezpiecznie usuń z widoku wszystkie metody, które obsługują wywołania modułu uruchamiającego intencję i aktywności.

  4. Funkcja signInRequest() zwraca teraz wartość JSONObject, więc nie musisz tworzyć PendingIntent. Zastąp zwrócony zamiar wartością JSONObject i wywołaj getCredential() z metod interfejsu API.

    suspend fun getPasskey(
        activity: Activity,
        creationResult: JSONObject
    ): GetCredentialResponse? {
        Toast.makeText(
            activity,
            "Fetching previously stored credentials",
            Toast.LENGTH_SHORT)
            .show()
        var result: GetCredentialResponse? = null
        try {
            val request= GetCredentialRequest(
                listOf(
                    GetPublicKeyCredentialOption(
                        creationResult.toString(),
                        null
                    ),
                    GetPasswordOption()
                )
            )
            result = credMan.getCredential(activity, request)
            if (result.credential is PublicKeyCredential) {
                val publicKeycredential = result.credential as PublicKeyCredential
                Log.i("TAG", "Passkey ${publicKeycredential.authenticationResponseJson}")
                return result
            }
        } catch (e: Exception) {
            showErrorAlert(activity, e)
        }
        return result
    }
    
  5. Po pomyślnym wywołaniu wyślij odpowiedź z powrotem na serwer, aby zweryfikować i uwierzytelnić użytkownika. Parametry żądania i odpowiedzi w przypadku tego wywołania interfejsu API są podobne do implementacji FIDO2, więc nie musisz wprowadzać żadnych zmian.

Dodatkowe materiały