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á.
Você vai trabalhar na sua própria versão do app.
- Acesse a página de edição do site em https://glitch.com/edit/#!/webauthn-codelab.
- 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.
- Copie o nome do projeto no canto superior esquerdo (você pode modificá-lo como quiser).
- Cole-o na seção
HOSTNAME
do arquivo.env
no glitch.
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.
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.
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".
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".
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.
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.
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.
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.
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.
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
- Especificação WebAuthn
- Introdução à API WebAuthn
- Workshop FIDO WebAuthn (link em inglês)
- Guia do WebAuthn: DUOSEC (em inglês)
Agradecimentos especiais a Yuriy Ackermann, da FIDO Alliance, pela ajuda.