La tua prima API FIDO2 per Android

1. Introduzione

Che cos'è l'API FIDO2?

L'API FIDO2 consente alle applicazioni Android di creare e utilizzare credenziali valide e attestate basate su chiavi pubbliche per autenticare gli utenti. L'API fornisce un'implementazione client WebAuthn, che supporta l'utilizzo di autenticatori di roaming BLE, NFC e USB (token di sicurezza), nonché un autenticatore di piattaforma, che consente all'utente di autenticarsi con la propria impronta o il proprio blocco schermo.

Cosa creerai...

In questo codelab, creerai un'app Android con una semplice funzionalità di riautenticazione mediante un sensore di impronte digitali. "Riautenticazione" si verifica quando un utente accede a un'app e esegue nuovamente l'autenticazione quando torna alla tua app o quando tenta di accedere a una sezione importante dell'app. Quest'ultimo caso è noto anche come "autenticazione step-up".

Cosa imparerai...

Imparerai a chiamare l'API FIDO2 di Android e le opzioni a tua disposizione per soddisfare le diverse occasioni. Scoprirai anche best practice specifiche per la riautenticazione.

Che cosa ti serve...

  • Dispositivo Android con sensore di impronte digitali (anche senza sensore di impronte digitali, il blocco schermo può fornire una funzionalità di verifica utente equivalente)
  • Android OS 7.0 o successivo con gli ultimi aggiornamenti. Assicurati di registrare un'impronta (o un blocco schermo).

2. Preparazione

Clona il repository

Dai un'occhiata al repository GitHub.

https://github.com/android/codelab-fido2

$ git clone https://github.com/android/codelab-fido2.git

Cosa implementeremo?

  • Consenti agli utenti di registrare un "utente che verifica l'autenticazione della piattaforma" (lo smartphone Android dotato del sensore di impronte digitali funzionerà come uno di questi).
  • Consenti agli utenti di eseguire nuovamente l'autenticazione all'app utilizzando la propria impronta.

Puoi visualizzare l'anteprima di ciò che creerai da qui.

Inizia il tuo progetto codelab

L'app completata invia richieste a un server all'indirizzo https://webauthn-codelab.glitch.me. Puoi provare la versione web della stessa app qui.

c2234c42ba8a6ef1.png

Lavorerai sulla tua versione dell'app.

  1. Vai alla pagina di modifica del sito web all'indirizzo https://glitch.com/edit/#!/webauthn-codelab.
  2. Cerca "Remix per modificare" nell'angolo in alto a destra. Se premi il pulsante, puoi "forchettare" il codice e continuare con la tua versione insieme a un nuovo URL del progetto. 9ef108869885e4ce.png
  3. Copia il nome del progetto in alto a sinistra (puoi modificarlo come preferisci). c91d0d59c61021a4.png
  4. Incollalo nella sezione HOSTNAME del file .env in glitch. 889b55b1cf74b894.png

3. Associa la tua app e un sito web a Digital Asset Links

Per utilizzare l'API FIDO2 su un'app per Android, devi associarla a un sito web e condividere le credenziali tra loro. A questo scopo, utilizza Digital Asset Links. Puoi dichiarare le associazioni ospitando un file JSON Digital Asset Links sul tuo sito web e aggiungendo un link al file Digital Asset Link al file manifest della tua app.

Ospita .well-known/assetlinks.json nel tuo dominio

Puoi definire un'associazione tra la tua app e il sito web creando un file JSON e posizionarlo all'indirizzo .well-known/assetlinks.json. Fortunatamente, esiste un codice server che visualizza automaticamente il file assetlinks.json, semplicemente aggiungendo i seguenti parametri di ambiente al file .env nel glitch:

  • ANDROID_PACKAGENAME: nome del pacchetto dell'app (com.example.android.fido2).
  • ANDROID_SHA256HASH: hash SHA256 del certificato di firma.

Per ottenere l'hash SHA256 del certificato di firma dello sviluppatore, utilizza il comando seguente. La password predefinita dell'archivio chiavi di debug è "android".

$ keytool -exportcert -list -v -alias androiddebugkey -keystore ~/.android/debug.keystore

Se accedi a https://<your-project-name>.glitch.me/.well-known/assetlinks.json , dovresti visualizzare una stringa JSON come la seguente:

[{
  "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:..."]
  }
}]

Apri il progetto in Android Studio

Fai clic su "Apri un progetto Android Studio esistente" nella schermata di benvenuto di Android Studio.

Scegli l'app "Android" all'interno del repository.

1062875cf11ffb95.png

Associa l'app al remix

Apri il file gradle.properties. Nella parte inferiore del file, cambia l'URL dell'host impostandolo sul remix Glitch che hai appena creato.

// ...

# The URL of the server
host=https://<your-project-name>.glitch.me

A questo punto, la configurazione di Digital Asset Links dovrebbe essere pronta.

4. Guarda come funziona ora l'app

Iniziamo controllando come funziona ora l'app. Assicurati di selezionare "app-start" nella casella combinata della configurazione di esecuzione. Fai clic su "Esegui". (il triangolare verde accanto alla casella combinata) per avviare l'app sul dispositivo Android connesso.

29351fb97062b43c.png

All'avvio dell'app viene visualizzata la schermata per digitare il tuo nome utente. Sono UsernameFragment. A scopo dimostrativo, l'app e il server accettano qualsiasi nome utente. Digita qualcosa e premi "Avanti".

bd9007614a9a3644.png

La prossima schermata che vedi è AuthFragment. Qui è dove l'utente può accedere con una password. In seguito aggiungeremo una funzionalità per accedere con FIDO2 qui. Anche in questo caso, a scopo dimostrativo, l'app e il server accettano qualsiasi password. Digita qualcosa e premi "Accedi".

d9caba817a0a99bd.png

Questa è l'ultima schermata di questa app, HomeFragment. Per il momento, qui viene visualizzato solo un elenco di credenziali vuoto. Premere "Nuova autorizzazione" ti riporta a AuthFragment. Premi "Esci" ti riporta a UsernameFragment. Il pulsante di azione mobile con "+" non serve a nulla ora, ma avvierà la registrazione di un

nuova credenziale dopo aver implementato il flusso di registrazione FIDO2.

1cfcc6c884020e37.png

Prima di iniziare a programmare, ecco una tecnica molto utile. Su Android Studio, premi "TODO" in basso. Verrà mostrato un elenco di tutti i TODO in questo codelab. Inizieremo con il primo TODO nella sezione successiva.

e5a811bbc7cd7b30.png

5. Registrare una credenziale tramite un'impronta digitale

Per attivare l'autenticazione mediante impronta, devi prima registrare una credenziale generata da un utente che verifica l'autenticazione della piattaforma, ovvero un autenticatore incorporato nel dispositivo che verifica l'utente tramite la biometria, ad esempio un sensore di impronte digitali.

37ce78fdf2759832.png

Come abbiamo visto nella sezione precedente, al momento il pulsante di azione mobile non funziona. Vediamo come registrare una nuova credenziale.

Chiama l'API server: /auth/registerRequest

Apri AuthRepository.kt e trova TODO(1).

In questo caso, registerRequest è il metodo chiamato quando viene premuto il FAB. Vorremmo fare in modo che questo metodo chiami l'API server /auth/registerRequest. L'API restituisce un ApiResult con tutti i PublicKeyCredentialCreationOptions necessari al client per generare una nuova credenziale.

Possiamo quindi chiamare getRegisterPendingIntent con le opzioni. Questa API FIDO2 restituisce un PendingIntent di Android per aprire una finestra di dialogo dell'impronta e generare una nuova credenziale, dopodiché possiamo restituire tale PendingIntent al chiamante.

Il metodo sarà il seguente.

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
}

Apri la finestra di dialogo dell'impronta per la registrazione

Apri HomeFragment.kt e trova TODO(2).

È qui che la UI recupera l'intent dal nostro AuthRepository. In questo caso useremo il membro createCredentialIntentLauncher per avviare l'PendingIntent ottenuto in seguito al passaggio precedente. Si aprirà una finestra di dialogo per la generazione delle credenziali.

binding.add.setOnClickListener {
  lifecycleScope.launch {
    val intent = viewModel.registerRequest()
    if (intent != null) {
      createCredentialIntentLauncher.launch(
        IntentSenderRequest.Builder(intent).build()
      )
    }
  }
}

Ricevi ActivityResult con la nuova credenziale

Apri HomeFragment.kt e trova TODO(3).

Questo metodo handleCreateCredentialResult viene chiamato dopo la chiusura della finestra di dialogo dell'impronta. Se una credenziale è stata generata correttamente, il membro data di ActivityResult conterrà le relative informazioni.

Dobbiamo innanzitutto estrarre una PublicKeyCredential dall'data. Data Intent ha un campo di array di byte aggiuntivo con la chiave Fido.FIDO2_KEY_CREDENTIAL_EXTRA. Puoi utilizzare un metodo statico in PublicKeyCredential denominato deserializeFromBytes per trasformare l'array di byte in un oggetto PublicKeyCredential.

Dopodiché controlla se il membro response di questo oggetto credenziali è un AuthenticationErrorResponse. In questo caso, si è verificato un errore durante la generazione della credenziale. altrimenti possiamo inviare le credenziali al nostro backend.

Il metodo finito sarà simile al seguente:

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)
      }
    }
  }
}

Chiama l'API server: /auth/registerResponse

Apri AuthRepository.kt e trova TODO(4).

Questo metodo registerResponse viene chiamato dopo che l'interfaccia utente ha generato correttamente una nuova credenziale e vogliamo inviarla nuovamente al server.

L'oggetto PublicKeyCredential contiene informazioni sulla credenziale appena generata. Ora vogliamo ricordare l'ID della nostra chiave locale in modo da poterla distinguere dalle altre chiavi registrate sul server. Nell'oggetto PublicKeyCredential, inserisci la relativa proprietà rawId in una variabile di stringa locale utilizzando toBase64.

Ora siamo pronti per inviare le informazioni al server. Utilizza api.registerResponse per chiamare l'API server e inviare la risposta. Il valore restituito contiene un elenco di tutte le credenziali registrate sul server, compresa quella nuova.

Infine, possiamo salvare i risultati nel nostro DataStore. L'elenco di credenziali deve essere salvato con la chiave CREDENTIALS impostata su StringSet. Puoi utilizzare toStringSet per convertire l'elenco di credenziali in un StringSet.

Inoltre, salviamo l'ID delle credenziali con la chiave 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)
  }
}

Esegui l'app: potrai fare clic sul FAB e registrare una nuova credenziale.

7d64d9289c5a3cbc.png

6. Autentica l'utente con un'impronta

Ora abbiamo una credenziale registrata sull'app e sul server. Ora possiamo utilizzarlo per consentire all'utente di accedere. Stiamo aggiungendo la funzionalità di accesso con impronta a AuthFragment. Quando un utente arriva sul sito, viene mostrata una finestra di dialogo dell'impronta. Una volta completata l'autenticazione, l'utente viene reindirizzato a HomeFragment.

Chiama l'API server: /auth/signinRequest

Apri AuthRepository.kt e trova TODO(5).

Questo metodo signinRequest viene chiamato all'apertura di AuthFragment. In questo caso vogliamo richiedere al server di verificare se è possibile consentire all'utente di accedere con FIDO2.

Dobbiamo innanzitutto recuperare PublicKeyCredentialRequestOptions dal server. Utilizza api.signInRequest per chiamare l'API server. Il valore ApiResult restituito contiene PublicKeyCredentialRequestOptions.

Con PublicKeyCredentialRequestOptions, possiamo utilizzare l'API FIDO2 getSignIntent per creare un PendingIntent per aprire la finestra di dialogo fingerprint.

Infine, possiamo riportare PendingIntent nella UI.

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
}

Apri la finestra di dialogo dell'impronta per l'asserzione

Apri AuthFragment.kt e trova TODO(6).

È praticamente uguale a quello che abbiamo fatto per la registrazione. Possiamo avviare la finestra di dialogo dell'impronta con il membro signIntentLauncher.

viewLifecycleOwner.lifecycleScope.launchWhenStarted {
  launch {
    viewModel.signinRequests.collect { intent ->
      signIntentLauncher.launch(
        IntentSenderRequest.Builder(intent).build()
      )
    }
  }
  launch {
    ...
  }
}

Gestire l'ActivityResult

Apri AuthFragment.kt e trova TODO(7).

Anche in questo caso, è la stessa procedura utilizzata per la registrazione. Possiamo estrarre l'oggetto PublicKeyCredential, verificare l'eventuale presenza di un errore e passarlo al ViewModel.

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)
      }
    }
  }
}

Chiama l'API server: /auth/signinResponse

Apri AuthRepository.kt e trova TODO(8).

L'oggetto PublicKeyCredential contiene un ID credenziale come keyHandle. Proprio come nel flusso di registrazione, salviamo il tag in una variabile stringa locale in modo da poterlo memorizzare in un secondo momento.

Ora possiamo chiamare l'API server con api.signinResponse. Il valore restituito contiene un elenco di credenziali.

A questo punto, l'accesso è riuscito. Dobbiamo memorizzare tutti i risultati in DataStore. L'elenco di credenziali deve essere archiviato come StringSet con la chiave CREDENTIALS. L'ID credenziale locale che abbiamo salvato sopra deve essere memorizzato come stringa con chiave LOCAL_CREDENTIAL_ID.

Infine, dobbiamo aggiornare lo stato di accesso in modo che la UI possa reindirizzare l'utente al HomeFragment. Per farlo, invia un oggetto SignInState.SignedIn a SharedFlow denominato signInStateMutable. Vogliamo anche chiamare refreshCredentials per recuperare le credenziali dell'utente in modo che vengano elencate nella UI.

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)
  }
}

Esegui l'app e fai clic su "Nuova autorizzazione". per aprire AuthFragment. A questo punto dovresti vedere una finestra di dialogo dell'impronta che ti chiede di accedere usando l'impronta.

45f81419f84952c8.png

Complimenti! Hai imparato a utilizzare l'API FIDO2 su Android per la registrazione e l'accesso.

7. Complimenti!

Hai completato correttamente il codelab. La tua prima API FIDO2 per Android.

Che cosa hai imparato

  • Come registrare una credenziale utilizzando un utente che verifica l'autenticazione della piattaforma.
  • Come autenticare un utente utilizzando un autenticatore registrato.
  • Opzioni disponibili per la registrazione di un nuovo autenticato.
  • Best practice UX per le nuove autorizzazioni utilizzando un sensore biometrico.

Passaggio successivo

  • Scopri come creare un'esperienza simile in un sito web.

Per imparare, prova il codelab Your first WebAuthn.

Risorse

Un ringraziamento speciale a Yuriy Ackermann di FIDO Alliance per il tuo aiuto.