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
- 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
- Otwórz moduł CredentialManager w Android Studio.
Sprawdzanie stanu początkowego aplikacji
Aby sprawdzić, jak działa początkowy stan aplikacji:
- Uruchom aplikację.
- 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ć.
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.
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ć obiektCreatePublicKeyCredentialRequest()
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 |
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. | |
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. | |
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). | |
To pole to opcjonalna, przyjazna dla użytkownika nazwa konta. | |
Podmiot korzystający z usługi musi odpowiadać szczegółom Twojego zgłoszenia. Zawiera te atrybuty:
| |
Lista dozwolonych algorytmów i typów kluczy. Ta lista musi zawierać co najmniej 1 element. | |
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 | |
Wskazuje, czy urządzenie powinno być podłączone do platformy, czy nie lub czy nie ma takiego wymogu. Ustaw tę wartość na | |
| Wskaż wartość |
Tworzenie danych logowania
- 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
.
- 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()
zwracatrue
, co oznacza, że serwer testowy zapisał klucz publiczny na przyszłość.- Ustaw flagę
setSignedInThroughPasskeys
natrue
. - 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 |
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. | |
Identyfikator danych logowania w wersji obiektu | |
Obiekt | |
Obiekt atesta kodowany za pomocą |
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łaniemcreatePassword
:
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.
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 |
Wyzwanie wygenerowane przez serwer w obiekcie | |
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 |
- 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 metodziegetSavedCredentials()
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
jakotrue
. W przeciwnym razie ustaw go nafalse
.
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 |
Identyfikator zakodowany w formacie Base64URL uwierzytelnionego klucza dostępu. | |
Identyfikator danych logowania w wersji obiektu | |
Obiekt | |
Obiekt | |
Obiekt | |
Obiekt |
- 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
.