Migracja z FIDO2 do menedżera danych logowania

Dzięki obsłudze kluczy dostępu, logowania sfederowanego i zewnętrznych dostawców uwierzytelniania Credential Manager jest zalecanym interfejsem API do uwierzytelniania na Androidzie, który zapewnia bezpieczne i wygodne środowisko, które umożliwia 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 kluczy dostępu przez integrację z interfejsem Credential Manager API. Ten dokument opisuje, jak przenieść projekt z FIDO2 do menedżera danych uwierzytelniających.

Powody przejścia z FIDO2 na menedżera danych uwierzytelniających

W większości przypadków należy przenieść dostawcę uwierzytelniania aplikacji na Androida do Menedżera danych logowania. Oto możliwe powody migracji do Menedżera danych logowania:

  • Obsługa kluczy dostępu: Menedżer danych logowania obsługuje klucze dostępu – 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 sfederowanego. Ułatwia to użytkownikom uwierzytelnianie w Twojej aplikacji niezależnie od ich preferowanej metody uwierzytelniania.
  • Obsługa zewnętrznych dostawców danych logowania: w Androidzie 14 i nowszych Menedżer danych logowania obsługuje wielu zewnętrznych dostawców danych logowania. Oznacza to, że użytkownicy mogą logować się w Twojej aplikacji za pomocą swoich danych logowania od innych dostawców.
  • Spójność wrażeń użytkowników: Menedżer danych logowania zapewnia bardziej spójną obsługę 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 Menedżera danych logowania, wykonaj poniższe czynności.

Zaktualizuj zależności

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

    plugins {
      //…
        id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
      //…
    }
    
  2. W pliku build.gradle w projekcie zaktualizuj zależności, aby używać menedżera danych logowania i uwierzytelniania w usługach Play.

    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 logowania:

    val credMan = CredentialManager.create(context)
    

Utwórz klucze

Musisz utworzyć nowy klucz dostępu, powiązać go z kontem użytkownika i zapisać go na serwerze, aby użytkownik mógł się zalogować za jego pomocą. Tę możliwość możesz skonfigurować w swojej aplikacji, aktualizując wywołania funkcji rejestrowania.

Rysunek 1. Ten rysunek przedstawia sposób wymiany danych między aplikacją a serwerem podczas tworzenia klucza dostępu za pomocą menedżera danych uwierzytelniających.
  1. Aby uzyskać niezbędne parametry wysyłane do metody createCredential() podczas tworzenia klucza dostępu, dodaj do wywołania serwera registerRequest() parametr name("residentKey").value("required") zgodnie ze specyfikacją WebAuthn.

    suspend fun registerRequest(sessionId: String ... {
        // ...
        .method("POST", jsonRequestBody {
            name("attestation").value("none")
            name("authenticatorSelection").objectValue {
                name("residentKey").value("required")
            }
        }).build()
        // ...
    }
    
  2. Ustaw typ return dla registerRequest(), a wszystkich funkcji podrzędnych na JSONObject.

    suspend fun registerRequest(sessionId: String): ApiResult<JSONObject> {
        val call = client.newCall(
            Request.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 wszelkie metody, które obsługują program uruchamiający intencje i wywołania wyników działań.

  4. Ponieważ registerRequest() zwraca teraz wartość JSONObject, nie musisz tworzyć PendingIntent. Zastąp zwróconą intencję wartością JSONObject. Zaktualizuj wywołania intencji, tak aby wywoływały 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 as CreateCredentialRequest,
                    activity
                ) as CreatePublicKeyCredentialResponse
            } catch (e: CreateCredentialException) {
    
                showErrorAlert(activity, e)
    
                return null
            }
            return response
        }
    
  5. Po pomyślnym wywołaniu wyślij odpowiedź z powrotem do serwera. Żądanie i odpowiedź dla tego wywołania są podobne do implementacji FIDO2, więc nie są wymagane żadne zmiany.

Uwierzytelniaj za pomocą kluczy dostępu

Po skonfigurowaniu tworzenia klucza dostępu możesz tak skonfigurować aplikację, aby umożliwiała użytkownikom logowanie się i uwierzytelnianie przy użyciu ich kluczy. W tym celu trzeba zaktualizować kod uwierzytelniania, aby obsługiwał wyniki Menedżera danych logowania, oraz zaimplementować funkcję uwierzytelniania za pomocą kluczy dostępu.

Rysunek 2. Proces uwierzytelniania klucza dostępu w Menedżerze danych logowania.
  1. Wywołanie żądania logowania do serwera umożliwiające uzyskanie informacji niezbędnych do wysłania żądania getCredential() jest takie samo jak w przypadku implementacji FIDO2. Nie musisz wprowadzać żadnych zmian.
  2. Podobnie jak w przypadku wywołania żądania zarejestrowania, zwrócona odpowiedź ma format 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 wszelkie metody obsługujące program uruchamiający intencje i wywołania wyników działań.

  4. Ponieważ signInRequest() zwraca teraz wartość JSONObject, nie musisz tworzyć PendingIntent. Zastąp zwróconą intencję wartością JSONObject i wywołaj metodę 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 do serwera, aby zweryfikować i uwierzytelnić użytkownika. Parametry żądania i odpowiedzi dla tego wywołania interfejsu API są podobne do implementacji FIDO2, więc nie trzeba wprowadzać żadnych zmian.

Dodatkowe materiały