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.
Lavorerai sulla tua versione dell'app.
- Vai alla pagina di modifica del sito web all'indirizzo https://glitch.com/edit/#!/webauthn-codelab.
- 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.
- Copia il nome del progetto in alto a sinistra (puoi modificarlo come preferisci).
- Incollalo nella sezione
HOSTNAME
del file.env
in glitch.
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.
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.
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".
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".
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.
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.
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.
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.
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.
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.