Sua primeira API Android FIDO2

1. Introdução

O que é a API FIDO2?

A API FIDO2 permite que os aplicativos Android criem e usem credenciais baseadas em chave pública fortes e atestadas com a finalidade de autenticar usuários. A API oferece uma implementação do cliente WebAuthn, que oferece suporte ao uso de autenticadores de roaming USB, NFC e BLE (chaves de segurança), além de um autenticador de plataforma, que permite ao usuário fazer a autenticação usando impressão digital ou bloqueio de tela.

O que você vai criar...

Neste codelab, você criará um app Android com uma funcionalidade simples de reautenticação usando o sensor de impressão digital. "Reautenticação" é quando um usuário faz login em um app e depois faz a autenticação novamente quando ele volta para o app ou tenta acessar uma seção importante dele. O último caso também é conhecido como "autenticação de acompanhamento".

O que você vai aprender…

Você vai aprender a chamar a API Android FIDO2 e as opções que podem ser fornecidas para atender a várias ocasiões. Você também vai aprender práticas recomendadas específicas para reautenticação.

O que é necessário…

  • Dispositivo Android com sensor de impressão digital. Mesmo sem um sensor, o bloqueio de tela pode oferecer funcionalidade equivalente para a verificação do usuário
  • SO Android 7.0 ou mais recente com as atualizações mais recentes. Registre uma impressão digital ou um bloqueio de tela.

2. Etapas da configuração

Clone o repositório

Confira o repositório do GitHub.

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

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

O que vamos implementar?

  • Permitir que os usuários registrem um "usuário que verifica o autenticador da plataforma" (o próprio smartphone Android com sensor de impressão digital funcionará como um só).
  • Permitir que os usuários se autentiquem novamente no app usando a impressão digital.

Clique aqui para ver o que você vai criar.

Iniciar seu projeto do codelab

O app concluído envia solicitações para um servidor em https://webauthn-codelab.glitch.me. Você pode testar a versão web do mesmo app lá.

c2234c42ba8a6ef1.png

Você vai trabalhar na sua própria versão do app.

  1. Acesse a página de edição do site em https://glitch.com/edit/#!/webauthn-codelab.
  2. Encontre a opção "Remixar para editar" no canto superior direito. Ao pressionar o botão, você pode "bifurcar" o código e continuar com sua própria versão junto com um novo URL do projeto. 9ef108869885e4ce.png
  3. Copie o nome do projeto no canto superior esquerdo (você pode modificá-lo como quiser). c91d0d59c61021a4.png
  4. Cole-o na seção HOSTNAME do arquivo .env no glitch. 889b55b1cf74b894.png

3. Associar seu app e um site ao Digital Asset Links

Para usar a API FIDO2 em um app Android, associe-a a um site e compartilhe credenciais entre eles. Para fazer isso, use os Links de recursos digitais. Para declarar associações, hospede um arquivo JSON do Digital Asset Links no seu site e adicione um link para o arquivo Digital Asset Link ao manifesto do app.

Hospede .well-known/assetlinks.json no seu domínio

Para definir uma associação entre o app e o site, crie um arquivo JSON e coloque-o em .well-known/assetlinks.json. Felizmente, temos um código de servidor que mostra o arquivo assetlinks.json automaticamente, apenas adicionando os seguintes parâmetros de ambiente ao arquivo .env no glitch:

  • ANDROID_PACKAGENAME: nome do pacote do app (com.example.android.fido2).
  • ANDROID_SHA256HASH: hash SHA256 do seu certificado de assinatura

Para receber o hash SHA256 do seu certificado de assinatura de desenvolvedor, use o comando abaixo. A senha padrão do keystore de depuração é "android".

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

Ao acessar https://<your-project-name>.glitch.me/.well-known/assetlinks.json , você verá uma string JSON como esta:

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

Abrir o projeto no Android Studio

Clique em "Open an existing Android Studio project" na tela inicial do Android Studio.

Escolha a opção "Android" pasta dentro do check-out do repositório.

1062875cf11ffb95.png

Associar o app ao seu remix

Abra gradle.properties. Na parte de baixo do arquivo, mude o URL do host para o remix do Glitch que você acabou de criar.

// ...

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

Neste ponto, sua configuração do Digital Asset Links está pronta.

4. Veja como o app funciona agora

Vamos começar conferindo como o app funciona agora. Selecione "app-start" na caixa de combinação de configuração de execução. Clique em "Executar" (o triangular verde ao lado da caixa de combinação) para iniciar o app no dispositivo Android conectado.

29351fb97062b43c.png

Ao iniciar o app, você verá a tela para digitar seu nome de usuário. Isso é UsernameFragment. Para fins de demonstração, o aplicativo e o servidor aceitam qualquer nome de usuário. Basta digitar algo e pressionar "Próxima".

bd9007614a9a3644.png

A próxima tela será AuthFragment. Aqui o usuário pode fazer login com uma senha. Mais adiante, vamos adicionar um recurso para fazer login com o FIDO2 aqui. Para fins de demonstração, o aplicativo e o servidor aceitam qualquer senha. Basta digitar algo e pressionar "Fazer login".

d9caba817a0a99bd.png

Esta é a última tela do app, HomeFragment. Por enquanto, só há uma lista vazia de credenciais aqui. Pressionar "Reauth" leva você de volta para AuthFragment. Pressionar "Sair" leva você de volta para UsernameFragment. O botão de ação flutuante com "+" não faz nada no momento, mas inicia o registro do

nova credencial depois de implementar o fluxo de registro FIDO2.

1cfcc6c884020e37.png

Antes de começar a programar, confira esta técnica útil. No Android Studio, pressione "TODO" na parte de baixo. Ela vai mostrar uma lista de todos os comentários feitos neste codelab. Começaremos com o primeiro TODO na próxima seção.

e5a811bbc7cd7b30.png

5. Registrar uma credencial usando uma impressão digital

Para ativar a autenticação usando uma impressão digital, primeiro é necessário registrar uma credencial gerada por um usuário que verifica o autenticador da plataforma, um autenticador incorporado ao dispositivo que verifica o usuário usando biometria, como um sensor de impressão digital.

37ce78fdf2759832.png

Como vimos na seção anterior, o botão de ação flutuante não faz nada agora. Vamos conferir como registrar uma nova credencial.

Chame a API do servidor: /auth/registerRequest

Abra AuthRepository.kt e encontre TODO(1).

Aqui, registerRequest é o método chamado quando o FAB é pressionado. Queremos fazer esse método chamar a API do servidor /auth/registerRequest. A API retorna um ApiResult com todos os PublicKeyCredentialCreationOptions necessários para o cliente gerar uma nova credencial.

Em seguida, podemos chamar getRegisterPendingIntent com as opções. Essa API FIDO2 retorna uma PendingIntent do Android para abrir uma caixa de diálogo de impressão digital e gerar uma nova credencial. É possível retornar essa PendingIntent ao autor da chamada.

O método ficará como mostrado a seguir.

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
}

Abrir a caixa de diálogo de impressão digital para registro

Abra HomeFragment.kt e encontre TODO(2).

É aqui que a interface recebe a intent do AuthRepository. Aqui, usaremos o membro createCredentialIntentLauncher para iniciar a PendingIntents que recebemos como resultado da etapa anterior. Isso vai abrir uma caixa de diálogo para a geração de credenciais.

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

Receber o ActivityResult com a nova credencial.

Abra HomeFragment.kt e encontre TODO(3).

Esse método handleCreateCredentialResult é chamado depois que a caixa de diálogo de impressão digital é fechada. Se uma credencial tiver sido gerada, o membro data do ActivityResult conterá as informações da credencial.

Primeiro, precisamos extrair uma PublicKeyCredential do data. A intent de dados tem um campo extra de matriz de bytes com a chave Fido.FIDO2_KEY_CREDENTIAL_EXTRA. É possível usar um método estático no PublicKeyCredential chamado deserializeFromBytes para transformar a matriz de bytes em um objeto PublicKeyCredential.

Em seguida, verifique se o membro response desse objeto de credencial é AuthenticationErrorResponse. Se estiver, ocorreu um erro ao gerar a credencial. caso contrário, podemos enviar a credencial para o back-end.

O método finalizado ficará assim:

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

Chame a API do servidor: /auth/registerResponse

Abra AuthRepository.kt e encontre TODO(4).

Esse método registerResponse é chamado depois que a interface gera uma nova credencial, e nós queremos enviá-la de volta ao servidor.

O objeto PublicKeyCredential contém informações sobre a credencial recém-gerada. Agora queremos lembrar o ID da nossa chave local para que possamos distingui-la de outras chaves registradas no servidor. No objeto PublicKeyCredential, pegue a propriedade rawId e coloque-a em uma variável de string local usando toBase64.

Agora estamos prontos para enviar as informações para o servidor. Use api.registerResponse para chamar a API do servidor e enviar a resposta. O valor retornado contém uma lista de todas as credenciais registradas no servidor, incluindo a nova.

Por fim, podemos salvar os resultados em DataStore. A lista de credenciais precisa ser salva com a chave CREDENTIALS como um StringSet. É possível usar toStringSet para converter a lista de credenciais em um StringSet.

Além disso, salvamos o ID da credencial com a chave 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)
  }
}

Execute o app. Você poderá clicar no FAB e registrar uma nova credencial.

7d64d9289c5a3cbc.png

6. Autenticar o usuário com uma impressão digital

Agora temos uma credencial registrada no app e no servidor. Agora, podemos usá-la para permitir que o usuário faça login. Estamos adicionando o recurso de login com impressão digital ao AuthFragment. Quando um usuário acessa ela, uma caixa de diálogo de impressão digital aparece. Quando a autenticação é bem-sucedida, o usuário é redirecionado para HomeFragment.

Chame a API do servidor: /auth/signinRequest

Abra AuthRepository.kt e encontre TODO(5).

Esse método signinRequest é chamado quando o AuthFragment é aberto. Aqui, queremos solicitar o servidor e verificar se podemos permitir que o usuário faça login com o FIDO2.

Primeiro, precisamos recuperar PublicKeyCredentialRequestOptions do servidor. Use api.signInRequest para chamar a API do servidor. O ApiResult retornado contém PublicKeyCredentialRequestOptions.

Com o PublicKeyCredentialRequestOptions, podemos usar a API FIDO2 getSignIntent para criar uma PendingIntent para abrir a caixa de diálogo de impressão digital.

Por fim, podemos retornar a PendingIntent de volta à interface.

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
}

Abrir a caixa de diálogo de impressão digital para declaração

Abra AuthFragment.kt e encontre TODO(6).

Isso é praticamente o mesmo que fizemos com o registro. Podemos abrir a caixa de diálogo de impressão digital com o membro signIntentLauncher.

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

Processar o ActivityResult

Abra AuthFragment.kt e encontre TODO(7).

Mais uma vez, é o mesmo que fizemos para o registro. Podemos extrair a PublicKeyCredential, verificar se há um erro e transmiti-lo ao 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)
      }
    }
  }
}

Chame a API do servidor: /auth/signinResponse

Abra AuthRepository.kt e encontre TODO(8).

O objeto PublicKeyCredential tem um ID de credencial como keyHandle. Assim como fizemos no fluxo de registro, vamos salvar isso em uma variável de string local para poder armazená-la mais tarde.

Agora estamos prontos para chamar a API do servidor com api.signinResponse. O valor retornado contém uma lista de credenciais.

Nesse momento, o login foi concluído. Precisamos armazenar todos os resultados em DataStore. A lista de credenciais precisa ser armazenada como StringSet com a chave CREDENTIALS. O ID da credencial local que salvamos acima precisa ser armazenado como uma string com a chave LOCAL_CREDENTIAL_ID.

Por fim, precisamos atualizar o estado de login para que a IU possa redirecionar o usuário para o HomeFragment. Para isso, emita um objeto SignInState.SignedIn chamado signInStateMutable para o SharedFlow. Também queremos chamar refreshCredentials para buscar as credenciais do usuário para que elas sejam listadas na interface.

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

Execute o app e clique em "Reauth" para abrir AuthFragment. Agora será exibida uma caixa de diálogo solicitando que você faça login com ela.

45f81419f84952c8.png

Parabéns! Agora você aprendeu a usar a API FIDO2 no Android para registro e login.

7. Parabéns!

Você concluiu o codelab: Sua primeira API Android FIDO2.

O que você aprendeu

  • Como registrar uma credencial usando um usuário que verifica o autenticador da plataforma.
  • Como autenticar um usuário usando um autenticador registrado.
  • Opções disponíveis para registrar um novo autenticador.
  • Práticas recomendadas de UX para reautenticação usando um sensor biométrico.

Próxima etapa

  • Aprenda a criar uma experiência semelhante em um site.

Para aprender, teste o codelab Seu primeiro WebAuthn.

Recursos

Agradecimentos especiais a Yuriy Ackermann, da FIDO Alliance, pela ajuda.