1. Wprowadzenie
Co to jest interfejs API FIDO2?
Interfejs FIDO2 API pozwala aplikacjom na Androida tworzyć i wykorzystywać silne, potwierdzone dane uwierzytelniające oparte na kluczu publicznym do uwierzytelniania użytkowników. Interfejs API udostępnia implementację klienta WebAuthn, która obsługuje stosowanie uwierzytelniania BLE, NFC i roamingu USB (kluczy bezpieczeństwa) oraz platformy uwierzytelniającej, która umożliwia uwierzytelnianie użytkownika przy użyciu odcisku palca lub blokady ekranu.
Co zbudujesz...
W ramach tego ćwiczenia w Codelabs utworzysz aplikację na Androida z prostą funkcją ponownego uwierzytelniania przy użyciu czytnika linii papilarnych. „Ponowne uwierzytelnianie” gdy użytkownik loguje się w aplikacji, a potem ponownie uwierzytelnia się, gdy wraca do aplikacji lub próbuje uzyskać dostęp do ważnej sekcji aplikacji. Ten drugi przypadek jest też określany jako „uwierzytelnianie stopniowe”.
Czego się nauczysz...
Dowiesz się, jak wywoływać interfejs API FIDO2 Androida i jakie opcje możesz zaoferować, aby dostosować go do różnych okazji. Poznasz też sprawdzone metody dotyczące ponownego uwierzytelniania.
Czego potrzebujesz...
- Urządzenie z Androidem z czytnikiem linii papilarnych (nawet bez czytnika linii papilarnych, blokada ekranu może oferować równoważną funkcję weryfikacji użytkownika)
- System operacyjny Android 7.0 lub nowszy z najnowszymi aktualizacjami. Pamiętaj, aby zarejestrować odcisk palca (lub blokadę ekranu).
2. Przygotowanie
Klonowanie repozytorium
Sprawdź repozytorium GitHub.
https://github.com/android/codelab-fido2
$ git clone https://github.com/android/codelab-fido2.git
Co wdrożymy?
- Zezwalaj użytkownikom na rejestrowanie „uwierzytelniania platformy uwierzytelniającej użytkownika weryfikującego” (telefon z Androidem z czytnikiem linii papilarnych będzie działać jak ten).
- Zezwalaj użytkownikom na ponowne uwierzytelnienie się w aplikacji za pomocą odcisku palca.
Podgląd tego, co chcesz utworzyć, możesz zobaczyć tutaj.
Rozpoczynanie projektu ćwiczeń w Codelabs
Ukończona aplikacja wysyła żądania do serwera https://webauthn-codelab.glitch.me. Możesz na nim wypróbować wersję internetową tej samej aplikacji.
Pracujesz nad własną wersją aplikacji.
- Otwórz stronę edycji witryny pod adresem https://glitch.com/edit/#!/webauthn-codelab.
- Znajdź opcję „Remiksuj, aby edytować” w prawym górnym rogu. Naciskając przycisk, możesz „rozwidleć” kod i kontynuować tworzenie własnej wersji z nowym adresem URL projektu.
- Skopiuj nazwę projektu u góry po lewej stronie (w razie potrzeby możesz ją zmienić).
- Wklej go w sekcji
HOSTNAME
pliku.env
w przypadku błędu.
3. powiązywanie aplikacji i witryny za pomocą linków do zasobów cyfrowych,
Aby używać interfejsu FIDO2 API w aplikacji na Androida, powiąż go ze stroną internetową i udostępniaj między nimi dane logowania. Aby to zrobić, skorzystaj z funkcji Digital Asset Links. Powiązania możesz zadeklarować, hostując w swojej witrynie plik JSON protokołu Digital Asset Links i dodając link do tego pliku do pliku manifestu aplikacji.
Hostowanie .well-known/assetlinks.json
w Twojej domenie
Powiązanie między aplikacją a witryną możesz zdefiniować, tworząc plik JSON i umieszczając go w tym miejscu: .well-known/assetlinks.json
. Na szczęście mamy kod serwera, który automatycznie wyświetla plik assetlinks.json
– wystarczy dodać w pliku .env
te parametry środowiska:
ANDROID_PACKAGENAME
: nazwa pakietu aplikacji (com.example.android.fido2)ANDROID_SHA256HASH
: identyfikator SHA256 Twojego certyfikatu podpisywania
Aby uzyskać identyfikator SHA256 certyfikatu podpisywania dewelopera, użyj tego polecenia. Domyślne hasło magazynu kluczy debugowania to „android”.
$ keytool -exportcert -list -v -alias androiddebugkey -keystore ~/.android/debug.keystore
Gdy otworzysz https://<your-project-name>.glitch.me/.well-known/assetlinks.json
, zobaczysz ciąg JSON podobny do tego:
[{
"relation": ["delegate_permission/common.handle_all_urls", "delegate_permission/common.get_login_creds"],
"target": {
"namespace": "web",
"site": "https://<your-project-name>.glitch.me"
}
}, {
"relation": ["delegate_permission/common.handle_all_urls", "delegate_permission/common.get_login_creds"],
"target": {
"namespace": "android_app",
"package_name": "com.example.android.fido2",
"sha256_cert_fingerprints": ["DE:AD:BE:EF:..."]
}
}]
Otwórz projekt w Android Studio
Kliknij „Otwórz istniejący projekt Android Studio”. na ekranie powitalnym Android Studio.
Wybierz aplikację „Android” w repozytorium.
Powiąż aplikację z remiksem
Otwórz plik gradle.properties
. U dołu pliku zmień adres URL hosta na utworzony przed chwilą remiks Glitch.
// ...
# The URL of the server
host=https://<your-project-name>.glitch.me
Na tym etapie konfiguracja Digital Asset Links powinna być już gotowa.
4. Zobacz, jak działa aplikacja
Zacznijmy od sprawdzenia, jak teraz działa aplikacja. Pamiętaj, aby wybrać „app-start” w polu kombi. Kliknij „Uruchom”. (zielony trójkąt obok pola wyboru), aby uruchomić aplikację na połączonym urządzeniu z Androidem.
Po uruchomieniu aplikacji zobaczysz ekran, na którym możesz wpisać swoją nazwę użytkownika. To jest UsernameFragment
. Na potrzeby zademonstrowania aplikacja i serwer akceptują każdą nazwę użytkownika. Po prostu coś wpisz i naciśnij „Dalej”.
Następny ekran to AuthFragment
. Dzięki temu użytkownik będzie mógł zalogować się za pomocą hasła. Później dodamy w tym miejscu funkcję logowania się za pomocą FIDO2. Dla celów demonstracyjnych również aplikacja i serwer akceptują dowolne hasło. Po prostu wpisz coś i naciśnij przycisk „Zaloguj się”.
To jest ostatni ekran tej aplikacji: HomeFragment
. Na razie zobaczysz tutaj tylko pustą listę danych logowania. Naciśnięcie „Reauth” wrócisz do: AuthFragment
. Naciśnięcie przycisku „Wyloguj się”. wrócisz do: UsernameFragment
. Pływający przycisk polecenia ze znakiem „+” nic teraz nie zrobi, ale rozpocznie rejestrację
nowych danych logowania po wdrożeniu procesu rejestracji FIDO2.
Zanim zaczniesz pisać kod, zapoznaj się z przydatną techniką. W Android Studio kliknij „TODO”. na dole. W tym ćwiczeniu z programowania wyświetli się lista wszystkich zadań do wykonania. W następnej sekcji zaczniemy od pierwszego zadania do wykonania.
5. Rejestrowanie danych logowania za pomocą odcisku palca
Aby włączyć uwierzytelnianie za pomocą odcisku palca, musisz najpierw zarejestrować dane logowania wygenerowane przez użytkownika weryfikującego aplikację uwierzytelniającą platformy – wbudowanego w urządzenia mechanizmu uwierzytelniającego, który weryfikuje użytkownika za pomocą danych biometrycznych, np. za pomocą czytnika linii papilarnych.
Jak pokazaliśmy w poprzedniej sekcji, pływający przycisk polecenia teraz nic nie robi. Zobaczmy, jak zarejestrować nowe dane logowania.
Wywołaj interfejs API serwera: /auth/registerRequest
Otwórz AuthRepository.kt
i znajdź TODO(1).
Tutaj registerRequest
jest metodą wywoływaną po naciśnięciu przycisku PPP. Chcemy, aby ta metoda wywoływała interfejs API serwera /auth/registerRequest
. Interfejs API zwraca wartość ApiResult
ze wszystkimi PublicKeyCredentialCreationOptions
potrzebnymi klientowi do wygenerowania nowych danych logowania.
Możemy wtedy zadzwonić pod numer getRegisterPendingIntent
i podać opcje. Ten interfejs API FIDO2 zwraca intencję PendingIntent z Androida, aby otworzyć okno dialogowe z odciskiem cyfrowym i wygenerować nowe dane uwierzytelniające. Możemy zwrócić tę intencję PendingIntent w celu wywołania metody.
Metoda będzie wtedy wyglądać tak:
suspend fun registerRequest(): PendingIntent? {
fido2ApiClient?.let { client ->
try {
val sessionId = dataStore.read(SESSION_ID)!!
when (val apiResult = api.registerRequest(sessionId)) {
ApiResult.SignedOutFromServer -> forceSignOut()
is ApiResult.Success -> {
if (apiResult.sessionId != null) {
dataStore.edit { prefs ->
prefs[SESSION_ID] = apiResult.sessionId
}
}
val task = client.getRegisterPendingIntent(apiResult.data)
return task.await()
}
}
} catch (e: Exception) {
Log.e(TAG, "Cannot call registerRequest", e)
}
}
return null
}
Otwórz okno z odciskami cyfrowymi na potrzeby rejestracji
Otwórz HomeFragment.kt
i znajdź TODO(2).
W tym miejscu interfejs użytkownika odzyskuje intencję z naszego AuthRepository
. W tym miejscu użyjemy użytkownika createCredentialIntentLauncher
, aby uruchomić intencję PendingIntent otrzymaną w wyniku poprzedniego kroku. Otworzy się okno do generowania danych logowania.
binding.add.setOnClickListener {
lifecycleScope.launch {
val intent = viewModel.registerRequest()
if (intent != null) {
createCredentialIntentLauncher.launch(
IntentSenderRequest.Builder(intent).build()
)
}
}
}
Otrzymuj ActivityResult przy użyciu nowych danych logowania
Otwórz HomeFragment.kt
i znajdź funkcję TODO(3).
Ta metoda handleCreateCredentialResult
jest wywoływana po zamknięciu okna odcisku palca. Jeśli dane logowania zostały wygenerowane, element data
elementu ActivityResult będzie zawierał informacje o danych logowania.
Najpierw musimy wyodrębnić PublicKeyCredential z data
. Intencja danych ma dodatkowe pole tablicy bajtów z kluczem Fido.FIDO2_KEY_CREDENTIAL_EXTRA
. Możesz użyć metody statycznej w PublicKeyCredential
o nazwie deserializeFromBytes
, aby przekształcić tablicę bajtów w obiekt PublicKeyCredential
.
Następnie sprawdź, czy elementem response
tego obiektu danych logowania jest AuthenticationErrorResponse
. Jeśli tak, podczas generowania danych logowania wystąpił błąd. W przeciwnym razie możemy wysłać dane logowania do naszego backendu.
Gotowe metody będą wyglądać tak:
private fun handleCreateCredentialResult(activityResult: ActivityResult) {
val bytes = activityResult.data?.getByteArrayExtra(Fido.FIDO2_KEY_CREDENTIAL_EXTRA)
when {
activityResult.resultCode != Activity.RESULT_OK ->
Toast.makeText(requireContext(), R.string.cancelled, Toast.LENGTH_LONG).show()
bytes == null ->
Toast.makeText(requireContext(), R.string.credential_error, Toast.LENGTH_LONG)
.show()
else -> {
val credential = PublicKeyCredential.deserializeFromBytes(bytes)
val response = credential.response
if (response is AuthenticatorErrorResponse) {
Toast.makeText(requireContext(), response.errorMessage, Toast.LENGTH_LONG)
.show()
} else {
viewModel.registerResponse(credential)
}
}
}
}
Wywołaj interfejs API serwera: /auth/registerResponse
Otwórz AuthRepository.kt
i znajdź TODO(4).
Ta metoda registerResponse
jest wywoływana po wygenerowaniu przez interfejs użytkownika nowych danych logowania i chcemy ją odesłać na serwer.
Obiekt PublicKeyCredential
zawiera informacje o nowo wygenerowanych danych logowania. Teraz chcemy zapamiętać identyfikator naszego klucza lokalnego, aby móc go odróżnić od innych kluczy zarejestrowanych na serwerze. W obiekcie PublicKeyCredential
użyj właściwości rawId
, a następnie umieść ją w lokalnej zmiennej ciągu znaków za pomocą funkcji toBase64
.
Teraz możemy wysłać informacje na serwer. Użyj metody api.registerResponse
, aby wywołać interfejs API serwera i odesłać odpowiedź. Zwrócona wartość zawiera listę wszystkich danych logowania zarejestrowanych na serwerze, w tym nowe dane.
Wyniki możemy też zapisać w naszym pliku DataStore
. Listę danych logowania należy zapisać z kluczem CREDENTIALS
jako StringSet
. Za pomocą toStringSet
możesz przekonwertować listę danych logowania na StringSet
.
Dodatkowo zapisujemy identyfikator danych logowania jako klucz LOCAL_CREDENTIAL_ID
.
suspend fun registerResponse(credential: PublicKeyCredential) {
try {
val sessionId = dataStore.read(SESSION_ID)!!
val credentialId = credential.rawId.toBase64()
when (val result = api.registerResponse(sessionId, credential)) {
ApiResult.SignedOutFromServer -> forceSignOut()
is ApiResult.Success -> {
dataStore.edit { prefs ->
result.sessionId?.let { prefs[SESSION_ID] = it }
prefs[CREDENTIALS] = result.data.toStringSet()
prefs[LOCAL_CREDENTIAL_ID] = credentialId
}
}
}
} catch (e: ApiException) {
Log.e(TAG, "Cannot call registerResponse", e)
}
}
Po uruchomieniu aplikacji będzie można kliknąć przycisk FAB i zarejestrować nowe dane logowania.
6. Uwierzytelnianie użytkownika odciskiem palca
Dane logowania zostały zarejestrowane w aplikacji i na serwerze. Teraz będziemy mogli go używać, aby umożliwić użytkownikowi zalogowanie się. Dodajemy do usługi AuthFragment
funkcję logowania odciskiem palca. Gdy użytkownik do nich trafi, pojawi się okno z odciskiem palca. Po pomyślnym uwierzytelnieniu użytkownik zostanie przekierowany na stronę HomeFragment
.
Wywołaj interfejs API serwera: /auth/signinRequest
Otwórz AuthRepository.kt
i znajdź TODO(5).
Ta metoda signinRequest
jest wywoływana po otwarciu AuthFragment
. W tym przypadku chcemy wysłać żądanie do serwera i sprawdzić, czy użytkownik może zalogować się za pomocą FIDO2.
Najpierw musimy pobrać plik PublicKeyCredentialRequestOptions
z serwera. Użyj api.signInRequest
, aby wywołać interfejs API serwera. Zwrócona wartość ApiResult
zawiera PublicKeyCredentialRequestOptions
.
Za pomocą PublicKeyCredentialRequestOptions
można użyć interfejsu FIDO2 API getSignIntent
, aby utworzyć intencję PendingIntent, która otworzy okno z odciskami cyfrowymi.
Na koniec możemy zwrócić intencję PendingIntent z powrotem do interfejsu użytkownika.
suspend fun signinRequest(): PendingIntent? {
fido2ApiClient?.let { client ->
val sessionId = dataStore.read(SESSION_ID)!!
val credentialId = dataStore.read(LOCAL_CREDENTIAL_ID)
if (credentialId != null) {
when (val apiResult = api.signinRequest(sessionId, credentialId)) {
ApiResult.SignedOutFromServer -> forceSignOut()
is ApiResult.Success -> {
val task = client.getSignPendingIntent(apiResult.data)
return task.await()
}
}
}
}
return null
}
Otwórz okno z odciskami cyfrowymi na potrzeby potwierdzenia
Otwórz AuthFragment.kt
i znajdź TODO(6).
Procedura przebiega identycznie jak przy rejestracji. Możemy uruchomić okno dialogowe odcisku palca z użytkownikiem signIntentLauncher
.
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
launch {
viewModel.signinRequests.collect { intent ->
signIntentLauncher.launch(
IntentSenderRequest.Builder(intent).build()
)
}
}
launch {
...
}
}
Obsługa wyniku ActivityResult
Otwórz plik AuthFragment.kt i znajdź TODO(7).
Powtórzenie rejestracji zostało takie samo jak w przypadku rejestracji. Możemy wyodrębnić PublicKeyCredential
, sprawdzić, czy nie ma błędu, i przekazać go do modelu widoku danych.
private fun handleSignResult(activityResult: ActivityResult) {
val bytes = activityResult.data?.getByteArrayExtra(Fido.FIDO2_KEY_CREDENTIAL_EXTRA)
when {
activityResult.resultCode != Activity.RESULT_OK ->
Toast.makeText(requireContext(), R.string.cancelled, Toast.LENGTH_SHORT).show()
bytes == null ->
Toast.makeText(requireContext(), R.string.auth_error, Toast.LENGTH_SHORT)
.show()
else -> {
val credential = PublicKeyCredential.deserializeFromBytes(bytes)
val response = credential.response
if (response is AuthenticatorErrorResponse) {
Toast.makeText(requireContext(), response.errorMessage, Toast.LENGTH_SHORT)
.show()
} else {
viewModel.signinResponse(credential)
}
}
}
}
Wywołaj interfejs API serwera: /auth/signinResponse
Otwórz AuthRepository.kt
i znajdź funkcję TODO(8).
W obiekcie PublicKeyCredential identyfikator danych logowania to keyHandle
. Tak jak podczas rejestracji, zapiszemy go w lokalnej zmiennej ciągu znaków, by móc ją później zapisać.
Możemy teraz wywoływać interfejs API serwera za pomocą polecenia api.signinResponse
. Zwrócona wartość zawiera listę danych logowania.
Na tym etapie logowanie się udało. Wszystkie wyniki musimy zapisać w naszej DataStore
. Listę danych logowania należy zapisać jako StringSet z kluczem CREDENTIALS
. Lokalny identyfikator danych logowania, który zapisaliśmy powyżej, powinien być przechowywany jako ciąg z kluczem LOCAL_CREDENTIAL_ID
.
Na koniec musimy zaktualizować stan logowania, aby interfejs użytkownika mógł przekierowywać użytkownika do fragmentu HomeFragment. Można to zrobić, wysyłając obiekt SignInState.SignedIn
do obiektu SharedFlow o nazwie signInStateMutable
. Chcemy również wywołać funkcję refreshCredentials
, by pobrać dane logowania użytkownika, tak by znalazły się one w interfejsie.
suspend fun signinResponse(credential: PublicKeyCredential) {
try {
val username = dataStore.read(USERNAME)!!
val sessionId = dataStore.read(SESSION_ID)!!
val credentialId = credential.rawId.toBase64()
when (val result = api.signinResponse(sessionId, credential)) {
ApiResult.SignedOutFromServer -> forceSignOut()
is ApiResult.Success -> {
dataStore.edit { prefs ->
result.sessionId?.let { prefs[SESSION_ID] = it }
prefs[CREDENTIALS] = result.data.toStringSet()
prefs[LOCAL_CREDENTIAL_ID] = credentialId
}
signInStateMutable.emit(SignInState.SignedIn(username))
refreshCredentials()
}
}
} catch (e: ApiException) {
Log.e(TAG, "Cannot call registerResponse", e)
}
}
Uruchom aplikację i kliknij „Reauth” aby otworzyć: AuthFragment
. Zobaczysz okno dialogowe z prośbą o zalogowanie się odciskiem palca.
Gratulacje! Wiesz już, jak zarejestrować się i logować, korzystając z interfejsu FIDO2 API na Androidzie.
7. Gratulacje!
Udało Ci się ukończyć ćwiczenia z programowania – Twój pierwszy interfejs API FIDO2 na Androida.
Czego się nauczyłeś?
- Jak zarejestrować dane logowania za pomocą użytkownika weryfikującego aplikację uwierzytelniającą platformy.
- Jak uwierzytelnić użytkownika przy użyciu zarejestrowanego mechanizmu uwierzytelniającego.
- Dostępne opcje rejestracji nowego modułu uwierzytelniającego.
- Sprawdzone metody UX w zakresie ponownego uwierzytelniania z użyciem czujnika biometrycznego.
Następny krok
- Dowiedz się, jak stworzyć podobne wrażenia w witrynie.
Aby się tego nauczyć, wykonaj Twoje pierwsze ćwiczenia WebAuthn w programie.
Zasoby
- Specyfikacja WebAuthn
- Wprowadzenie do WebAuthn API
- Warsztaty FIDO WebAuthn
- Przewodnik po WebAuthn: DUOSEC
Specjalne podziękowania za pomoc dla Yuriy Ackermann z FIDO Alliance.