Wersja 2024 Q4: Dowiedz się, jak uprościć proces uwierzytelniania za pomocą interfejsu Credential Manager API w aplikacji na Androida

1. Zanim zaczniesz

Tradycyjne rozwiązania uwierzytelniania stwarzają wiele problemów związanych z bezpieczeństwem i użytecznością.

Hasła są powszechnie używane, ale...

  • łatwo zapomniane,
  • Użytkownicy muszą znać zasady tworzenia silnych haseł.
  • Łatwo je przechwycić, zebrać i odtworzyć.

Zespół Androida pracował nad interfejsem Credential Manager API, aby uprościć logowanie i rozwiązać problemy z bezpieczeństwem. W tym celu udostępniliśmy klucze dostępu, czyli nowy standard branżowy uwierzytelniania bez hasła.

Menedżer danych logowania obsługuje klucze dostępu i łączy je z tradycyjnymi metodami uwierzytelniania, takimi jak hasła czy logowanie przez Google.

Użytkownicy będą mogli tworzyć klucze dostępu i przechowywać je w Menedżerze haseł Google, który będzie synchronizował te klucze na urządzeniach z Androidem, na których użytkownik jest zalogowany. Klucz dostępu musi zostać utworzony, powiązany z kontem użytkownika i mieć klucz publiczny przechowywany na serwerze, zanim użytkownik będzie mógł się zalogować.

W tym laboratorium programistycznym dowiesz się, jak rejestrować się przy użyciu kluczy dostępu i hasła za pomocą interfejsu Credential Manager API oraz jak używać ich do uwierzytelniania. Istnieją 2 procesy:

  • Rejestracja : za pomocą kluczy dostępu i hasła.
  • Logowanie : za pomocą kluczy dostępu i zapisanych haseł.

Wymagania wstępne

  • podstawową znajomość uruchamiania aplikacji w Android Studio;
  • podstawową znajomość procesu uwierzytelniania w aplikacjach na Androida;
  • podstawowa znajomość kluczy dostępu,

Czego się nauczysz

  • Jak utworzyć klucz dostępu
  • Jak zapisać hasło w menedżerze haseł.
  • Jak uwierzytelniać użytkowników za pomocą klucza dostępu lub zapisanego hasła.

Czego potrzebujesz

Jedna z tych kombinacji urządzeń:

  • urządzenie z Androidem 9 lub nowszym (w przypadku kluczy dostępu) i Androidem 4.4 lub nowszym(w przypadku uwierzytelniania za pomocą hasła przez interfejs API menedżera danych logowania).
  • Urządzenie z czujnikiem biometrycznym.
  • Zarejestruj dane biometryczne (lub blokadę ekranu).
  • Wersja wtyczki Kotlin : 1.8.10

2. Konfiguracja

  1. Sklonuj to repozytorium na laptopie z gałęzi credman_codelab : https://github.com/android/identity-samples/tree/credman_codelab
git clone -b credman_codelab https://github.com/android/identity-samples.git
  1. Otwórz moduł CredentialManager w Android Studio.

Sprawdzanie stanu początkowego aplikacji

Aby sprawdzić, jak działa początkowy stan aplikacji:

  1. Uruchom aplikację.
  2. Widnieje ekran główny z przyciskami rejestracji i logowania. Te przyciski nie są jeszcze aktywne, ale w następnych sekcjach opiszemy, jak z nich korzystać.

7a6fe80f4cf877a8.jpeg

3. Dodanie możliwości rejestracji za pomocą kluczy dostępu

Podczas rejestracji nowego konta w aplikacji na Androida, która korzysta z interfejsu Credential Manager API, użytkownicy mogą utworzyć klucz dostępu do swojego konta. Klucz dostępu będzie bezpiecznie przechowywany u wybranego dostawcy danych logowania, a użytkownik będzie mógł używać go przy kolejnych logowaniach bez konieczności wpisywania hasła za każdym razem.

Teraz utwórz klucz dostępu i zarejestruj dane logowania użytkownika za pomocą funkcji biometrycznej lub blokady ekranu.

Rejestracja za pomocą klucza dostępu

Kod w pliku Credential Manager/app/main/java/SignUpFragment.kt definiuje pole tekstowe „username” i przycisk do rejestracji za pomocą klucza dostępu.

1f4c50daa2551f1.jpeg

Przekazywanie wyzwania i innej odpowiedzi w formacie JSON do wywołania createPasskey().

Zanim utworzysz klucz dostępu, musisz poprosić serwer o wygenerowanie informacji, które zostaną przekazane do interfejsu Credential Manager API podczas wywołania metody createCredential().

W zasobach projektu masz już przygotowaną odpowiedź testową o nazwie RegFromServer.txt, która zwraca niezbędne parametry w tym CodeLab.

  • W aplikacji otwórz plik SignUpFragment.kt i znajdź metodę signUpWithPasskeys, w której umieścisz logikę tworzenia klucza dostępu i logowania użytkownika. Metodę znajdziesz w tej samej klasie.
  • Sprawdź blok else z komentarzem, aby wywołać funkcję createPasskey(), i zastąp go tym kodem:

SignUpFragment.kt

//TODO : Call createPasskey() to signup with passkey

val data = createPasskey()

Ta metoda zostanie wywołana, gdy na ekranie pojawi się prawidłowa nazwa użytkownika.

  • W ramach metody createPasskey() musisz utworzyć obiekt CreatePublicKeyCredentialRequest() z wymaganymi parametrami zwracanymi.

SignUpFragment.kt

//TODO create a CreatePublicKeyCredentialRequest() with necessary registration json from server

val request = CreatePublicKeyCredentialRequest(fetchRegistrationJsonFromServer())

Metoda fetchRegistrationJsonFromServer() odczytuje z zasobów emulowaną odpowiedź serwera PublicKeyCredentialCreationOptions w formacie JSON i zwróci obiekt JSON rejestracji, który ma być przekazany podczas tworzenia klucza dostępu.

  • Znajdź metodę fetchRegistrationJsonFromServer() i zastąp TODO tym kodem, aby zwracać JSON. Usuń instrukcję zwracania pustego ciągu :

SignUpFragment.kt

//TODO fetch registration mock response

val response = requireContext().readFromAsset("RegFromServer")

//Update userId,challenge, name and Display name in the mock
return response.replace("<userId>", getEncodedUserId())
   .replace("<userName>", binding.username.text.toString())
   .replace("<userDisplayName>", binding.username.text.toString())
   .replace("<challenge>", getEncodedChallenge())
  • Ten plik JSON jest niekompletny i zawiera 4 pola, które trzeba zastąpić.
  • Identyfikator użytkownika musi być unikalny, aby użytkownik mógł utworzyć kilka kluczy dostępu (w razie potrzeby). Zastąp <userId> wygenerowaną wartością userId.
  • <challenge> musi być też unikalny, aby można było wygenerować losowe, unikalne wyzwanie. Metoda jest już w Twoim kodzie.

Odpowiedź serwera PublicKeyCredentialCreationOptions może zawierać więcej opcji. Poniżej znajdziesz przykłady niektórych z tych pól:

{
  "challenge": String,
  "rp": {
    "name": String,
    "id": String
  },
  "user": {
    "id": String,
    "name": String,
    "displayName": String
  },
  "pubKeyCredParams": [
    {
      "type": "public-key",
      "alg": -7
    },
    {
      "type": "public-key",
      "alg": -257
    }
  ],
  "timeout": 1800000,
  "attestation": "none",
  "excludeCredentials": [],
  "authenticatorSelection": {
    "authenticatorAttachment": "platform",
    "requireResidentKey": true,
    "residentKey": "required",
    "userVerification": "required"
  }
}

W tabeli poniżej opisano niektóre ważne parametry obiektu PublicKeyCredentialCreationOptions:

Parametry

Teksty reklam

challenge

losowy ciąg znaków wygenerowany przez serwer, który zawiera wystarczającą ilość entropii, aby uniemożliwić jego odgadnięcie. Musi mieć co najmniej 16 bajtów. Jest on wymagany, ale nieużywany podczas rejestracji, chyba że przeprowadzasz potwierdzenie.

user.id

Unikalny identyfikator użytkownika. Ta wartość nie może zawierać informacji umożliwiających identyfikację, takich jak adresy e-mail czy nazwy użytkowników. Dobrze sprawdzi się losowa wartość 16-bajtowa wygenerowana dla każdego konta.

user.name

To pole powinno zawierać unikalny identyfikator konta, który użytkownik będzie w stanie rozpoznać, np. adres e-mail lub nazwę użytkownika. Będzie ona widoczna w selektorze kont. (jeśli używasz nazwy użytkownika, użyj tej samej wartości co w przypadku uwierzytelniania za pomocą hasła).

user.displayName

To pole to opcjonalna, przyjazna dla użytkownika nazwa konta.

rp.id

Podmiot korzystający z usługi musi odpowiadać szczegółom Twojego zgłoszenia. Zawiera te atrybuty:

  • name (wymagana): nazwa aplikacji
  • ID (opcjonalnie): odpowiada domenie lub subdomenie. Jeśli nie ma takiej wartości, używana jest bieżąca domena.
  • icon (opcjonalnie).

pubKeyCredParams

Lista dozwolonych algorytmów i typów kluczy. Ta lista musi zawierać co najmniej 1 element.

excludeCredentials

Użytkownik, który próbuje zarejestrować urządzenie, może mieć zarejestrowane inne urządzenia. Aby ograniczyć tworzenie wielu danych logowania na tym samym koncie w jednej aplikacji uwierzytelniającej, możesz zignorować te urządzenia. Element transports, jeśli jest podany, powinien zawierać wynik wywołania funkcji getTransports() podczas rejestracji poszczególnych danych logowania.

authenticatorSelection.authenticatorAttachment

Wskazuje, czy urządzenie powinno być podłączone do platformy, czy nie lub czy nie ma takiego wymogu. Ustaw tę wartość na platform. Oznacza to, że chcesz użyć uwierzytelniania za pomocą urządzenia z systemem, a użytkownik nie będzie musiał wkładać np. klucza bezpieczeństwa USB.

residentKey

Wskaż wartość required, aby utworzyć klucz dostępu.

Tworzenie danych logowania

  1. Po utworzeniu CreatePublicKeyCredentialRequest() musisz wywołać funkcję createCredential() z utworzonym żądaniem.

SignUpFragment.kt

//TODO call createCredential() with createPublicKeyCredentialRequest

try {
   response = credentialManager.createCredential(
       requireActivity(),
       request
   ) as CreatePublicKeyCredentialResponse
} catch (e: CreateCredentialException) {
   configureProgress(View.INVISIBLE)
   handlePasskeyFailure(e)
}
  • Przesyłasz wymagane informacje do usługi createCredential().
  • Gdy żądanie zostanie zrealizowane, na dole ekranu pojawi się karta z prośbą o utworzenie klucza dostępu.
  • Użytkownicy mogą teraz potwierdzać swoją tożsamość za pomocą danych biometrycznych lub blokady ekranu.
  • Zajmujesz się widocznością renderowanych widoków i wyjątkami, jeśli prośba o wyświetlenie nie powiedzie się z jakiegoś powodu. Komunikaty o błędach są rejestrowane i wyświetlane w oknie błędu aplikacji. Pełne logi błędów możesz sprawdzić w Android Studio lub za pomocą polecenia adb debug.

1ea8ace66135de1e.png

  1. Na koniec musisz dokończyć proces rejestracji. Aplikacja wysyła na serwer klucz publiczny, który jest rejestrowany dla bieżącego użytkownika.

Tutaj użyliśmy serwera testowego, więc zwracamy wartość true, która wskazuje, że serwer zapisał zarejestrowany klucz publiczny na potrzeby przyszłego uwierzytelniania i weryfikacji. Więcej informacji o rejestrowaniu kluczy dostępu po stronie serwera znajdziesz w dokumentacji dotyczącej implementacji.

W metodzie signUpWithPasskeys() odszukaj odpowiedni komentarz i zastąp go tym kodem:

SignUpFragment.kt

//TODO : complete the registration process after sending public key credential to your server and let the user in

data?.let {
   registerResponse()
   DataProvider.setSignedInThroughPasskeys(true)
   listener.showHome()
}
  • registerResponse() zwraca true, co oznacza, że serwer testowy zapisał klucz publiczny na przyszłość.
  • Ustaw flagę setSignedInThroughPasskeys na true.
  • Po zalogowaniu użytkownik jest przekierowywany na ekran główny.

Prawdziwy PublicKeyCredential może zawierać więcej pól. Poniżej znajdziesz przykład tych pól:

{
  "id": String,
  "rawId": String,
  "type": "public-key",
  "response": {
    "clientDataJSON": String,
    "attestationObject": String,
  }
}

W tabeli poniżej opisano niektóre ważne parametry obiektu PublicKeyCredential:

Parametry

Teksty reklam

id

Identyfikator utworzonego klucza dostępu zakodowany w formacie Base64URL. Ten identyfikator pomaga przeglądarce określić, czy po uwierzytelnieniu na urządzeniu znajduje się pasujący klucz dostępu. Ta wartość musi być przechowywana w bazie danych na zapleczu.

rawId

Identyfikator danych logowania w wersji obiektu ArrayBuffer.

response.clientDataJSON

Obiekt ArrayBuffer zawiera zakodowane dane klienta.

response.attestationObject

Obiekt atesta kodowany za pomocą ArrayBuffer. Zawiera on ważne informacje, takie jak identyfikator RP, flagi i klucz publiczny.

Uruchom aplikację, a potem kliknij przycisk Zarejestruj się przy użyciu kluczy dostępu i utwórz klucz dostępu.

4. Zapisywanie hasła w usługach dostawcy danych logowania

W tej aplikacji na ekranie rejestracji masz już implementowaną rejestrację z nazwą użytkownika i hasłem na potrzeby demonstracji.

Aby zapisać dane logowania użytkownika u dostawcy haseł, zaimplementuj CreatePasswordRequest, aby przekazać je do createCredential() w celu zapisania hasła.

  • Znajdź metodę signUpWithPassword() i zastąp TODO wywołaniem createPassword:

SignUpFragment.kt

//TODO : Save the user credential password with their password provider

createPassword()
  • W ramach metody createPassword() musisz utworzyć prośbę o hasło w taki sposób: zastąp TODO tym kodem:

SignUpFragment.kt

//TODO : CreatePasswordRequest with entered username and password

val request = CreatePasswordRequest(
   binding.username.text.toString(),
   binding.password.text.toString()
)
  • Następnie w ramach metody createPassword() utwórz dane logowania z żądaniem utworzenia hasła i zapisz dane logowania użytkownika u jego dostawcy haseł. Zastąp TODO tym kodem:

SignUpFragment.kt

//TODO : Create credential with created password request


try {
   credentialManager.createCredential(requireActivity(), request) as CreatePasswordResponse
} catch (e: Exception) {
   Log.e("Auth", " Exception Message : " + e.message)
}
  • Zapisane dane logowania do usługi uwierzytelniania za pomocą hasła umożliwiają uwierzytelnianie za pomocą hasła jednym kliknięciem.

5. Dodaliśmy możliwość uwierzytelniania za pomocą klucza lub hasła.

Teraz możesz używać tego klucza do bezpiecznego uwierzytelniania w aplikacji.

76e81460b26f9798.png

Pobierz wyzwanie i inne opcje do przekazania do wywołania getPasskey().

Zanim poprosisz użytkownika o uwierzytelnienie, musisz poprosić o parametry, które należy przekazać w formacie JSON WebAuthn z serwera, w tym wyzwanie.

W zasobach masz już przygotowaną odpowiedź (plik AuthFromServer.txt), która zwraca takie parametry w tym Codelab.

  • W aplikacji przejdź do pliku SignInFragment.kt i znajdź metodę signInWithSavedCredentials, w której napiszesz logikę uwierzytelniania za pomocą zapisanego klucza dostępu lub hasła i zezwalaj użytkownikowi na zalogowanie się:
  • Sprawdź blok else z komentarzem, aby wywołać funkcję createPasskey(), i zastąp go tym kodem:

SignInFragment.kt

//TODO : Call getSavedCredentials() method to signin using passkey/password

val data = getSavedCredentials()
  • W metodzie getSavedCredentials() musisz utworzyć obiekt GetPublicKeyCredentialOption() z niezbędnymi parametrami, które są wymagane do uzyskania danych logowania od dostawcy danych logowania.

SigninFragment.kt

//TODO create a GetPublicKeyCredentialOption() with necessary registration json from server

val getPublicKeyCredentialOption =
   GetPublicKeyCredentialOption(fetchAuthJsonFromServer(), null)

Metoda fetchAuthJsonFromServer() odczytuje odpowiedź JSON uwierzytelniania z zasobów i zwróci JSON uwierzytelniania, aby pobrać wszystkie klucze dostępu powiązane z tym kontem użytkownika.

Drugim parametrem funkcji GetPublicKeyCredentialOption() jest clientDataHash – skrót, który służy do weryfikacji tożsamości strony ufającej. Ustaw tę wartość tylko wtedy, gdy ustawisz wartość GetCredentialRequest.origin. W przypadku przykładowej aplikacji jest to null.

  • Znajdź metodę fetchAuthJsonFromServer() i zastąp TODO tym kodem, aby zwracać json, a także usuń instrukcję zwracania pustego ciągu:

SignInFragment.kt

//TODO fetch authentication mock json

return requireContext().readFromAsset("AuthFromServer")

Uwaga : serwer tego modułu Codelab został zaprojektowany tak, aby zwracać kod JSON, który jest jak najbardziej zbliżony do słownika PublicKeyCredentialRequestOptions przekazanego do wywołania getCredential() interfejsu API. Ten fragment kodu zawiera kilka przykładowych opcji, które możesz otrzymać w rzeczywistej odpowiedzi:

{
  "challenge": String,
  "rpId": String,
  "userVerification": "",
  "timeout": 1800000
}

W tabeli poniżej opisano niektóre ważne parametry obiektu PublicKeyCredentialRequestOptions:

Parametry

Teksty reklam

challenge

Wyzwanie wygenerowane przez serwer w obiekcie ArrayBuffer. Jest to wymagane, aby zapobiec atakom typu replay. Nigdy nie akceptuj tego samego wyzwania dwukrotnie. Możesz go traktować jako token CSRF.

rpId

Identyfikator RP to domena. Witryna może określać swoją domenę lub sufiks, który można zarejestrować. Ta wartość musi być zgodna z parametrem rp.id użytym podczas tworzenia klucza dostępu.

  • Następnie musisz utworzyć obiekt PasswordOption(), aby pobrać wszystkie zapisane hasła w dostawcy haseł za pomocą interfejsu Credential Manager API na tym koncie użytkownika. W metodzie getSavedCredentials() odszukaj TODO i zastąp go tym fragmentem kodu:

SigninFragment.kt

//TODO create a PasswordOption to retrieve all the associated user's password

val getPasswordOption = GetPasswordOption()

Uzyskaj dane logowania

  • Następnie musisz wywołać getCredential() z wszystkimi powyższymi opcjami, aby pobrać powiązane poświadczenia:

SignInFragment.kt

//TODO call getCredential() with required credential options

val result = try {
   credentialManager.getCredential(
       requireActivity(),
       GetCredentialRequest(
           listOf(
               getPublicKeyCredentialOption,
               getPasswordOption
           )
     )
   )
} catch (e: Exception) {
   configureViews(View.INVISIBLE, true)
   Log.e("Auth", "getCredential failed with exception: " + e.message.toString())
   activity?.showErrorAlert(
       "An error occurred while authenticating through saved credentials. Check logs for additional details"
   )
   return null
}

if (result.credential is PublicKeyCredential) {
   val cred = result.credential as PublicKeyCredential
   DataProvider.setSignedInThroughPasskeys(true)
   return "Passkey: ${cred.authenticationResponseJson}"
}
if (result.credential is PasswordCredential) {
   val cred = result.credential as PasswordCredential
   DataProvider.setSignedInThroughPasskeys(false)
   return "Got Password - User:${cred.id} Password: ${cred.password}"
}
if (result.credential is CustomCredential) {
   //If you are also using any external sign-in libraries, parse them here with the utility functions provided.
}

  • Przesyłasz wymagane informacje do usługi getCredential(). Ta funkcja pobiera listę opcji danych logowania i kontekst aktywności, aby renderować opcje w dolnej części ekranu w tym kontekście.
  • Po pomyślnym przesłaniu żądania na ekranie pojawi się panel z informacjami o wszystkich utworzonych danych logowania dla powiązanego konta.
  • Użytkownicy mogą teraz potwierdzić swoją tożsamość za pomocą danych biometrycznych lub blokady ekranu, aby uwierzytelnić wybrane dane logowania.
  • Jeśli wybrane dane logowania to PublicKeyCredential, ustaw flagę setSignedInThroughPasskeys jako true. W przeciwnym razie ustaw go na false.

Ten fragment kodu zawiera przykładowy obiekt PublicKeyCredential:

{
  "id": String
  "rawId": String
  "type": "public-key",
  "response": {
    "clientDataJSON": String
    "authenticatorData": String
    "signature": String
    "userHandle": String
  }
}

Tabela poniżej nie jest wyczerpująca, ale zawiera ważne parametry obiektu PublicKeyCredential:

Parametry

Teksty reklam

id

Identyfikator zakodowany w formacie Base64URL uwierzytelnionego klucza dostępu.

rawId

Identyfikator danych logowania w wersji obiektu ArrayBuffer.

response.clientDataJSON

Obiekt ArrayBuffer z danymi klienta. To pole zawiera informacje, takie jak wyzwanie i źródło, które serwer RP musi zweryfikować.

response.authenticatorData

Obiekt ArrayBuffer z danymi uwierzytelniania. To pole zawiera informacje takie jak identyfikator RP.

response.signature

Obiekt ArrayBuffer podpisu. Ta wartość jest podstawą danych logowania i musi zostać zweryfikowana na serwerze.

response.userHandle

Obiekt ArrayBuffer zawierający identyfikator użytkownika ustawiony w momencie utworzenia. Ta wartość może być używana zamiast identyfikatora danych logowania, jeśli serwer musi wybrać używane wartości identyfikatorów lub jeśli backend chce uniknąć tworzenia indeksu na podstawie identyfikatorów danych logowania.

  • Na koniec musisz przejść proces uwierzytelniania. Zwykle po tym, jak użytkownik uwierzytelni się za pomocą klucza dostępu, aplikacja wysyła na serwer klucz publiczny zawierający oświadczenie uwierzytelniające, które jest weryfikowane, a użytkownik jest uwierzytelniany.

W tym przypadku użyliśmy serwera testowego, więc zwracamy tylko wartość true, która wskazuje, że serwer zweryfikował twierdzenie. Więcej informacji o uwierzytelnianiu za pomocą klucza po stronie serwera znajdziesz w sekcji dotyczącej implementacji.

W metodie signInWithSavedCredentials() odszukaj odpowiedni komentarz i zastąp go tym kodem:

SignInFragment.kt

//TODO : complete the authentication process after validating the public key credential to your server and let the user in.

data?.let {
   sendSignInResponseToServer()
   listener.showHome()
}
  • sendSigninResponseToServer() zwraca wartość true (prawda), co oznacza, że (mockowy) serwer zweryfikował klucz publiczny na potrzeby przyszłego użycia.
  • Po zalogowaniu użytkownik jest przekierowywany na ekran główny.

Uruchom aplikację i otwórz Zaloguj się > Zaloguj się za pomocą klucza dostępu lub zapisanego hasła, a następnie spróbuj zalogować się, używając zapisanych danych logowania.

Wypróbuj

W aplikacji na Androida zaimplementowano tworzenie kluczy dostępu, zapisywanie hasła w Menedżerze danych logowania i uwierzytelnianie za pomocą kluczy dostępu lub zapisanego hasła za pomocą interfejsu Credential Manager API.

6. Gratulacje!

To ćwiczenie zostało ukończone. Jeśli chcesz sprawdzić ostateczne rozwiązanie, które jest dostępne pod adresem https://github.com/android/identity-samples/tree/main/CredentialManager

Jeśli masz pytania, zadaj je na Stack Overflow, dodając tag passkey.

Więcej informacji