Twój pierwszy interfejs API FIDO2 na Androida

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.

c2234c42ba8a6ef1.png

Pracujesz nad własną wersją aplikacji.

  1. Otwórz stronę edycji witryny pod adresem https://glitch.com/edit/#!/webauthn-codelab.
  2. 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. 9ef108869885e4ce.png
  3. Skopiuj nazwę projektu u góry po lewej stronie (w razie potrzeby możesz ją zmienić). c91d0d59c61021a4.png
  4. Wklej go w sekcji HOSTNAME pliku .env w przypadku błędu. 889b55b1cf74b894.png

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.

1062875cf11ffb95.png

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.

29351fb97062b43c.png

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”.

bd9007614a9a3644.png

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ę”.

d9caba817a0a99bd.png

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.

1cfcc6c884020e37.png

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.

e5a811bbc7cd7b30.png

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.

37ce78fdf2759832.png

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.

7d64d9289c5a3cbc.png

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.

45f81419f84952c8.png

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

Specjalne podziękowania za pomoc dla Yuriy Ackermann z FIDO Alliance.