Aprenda a simplificar as jornadas de autenticação usando a API Credential Manager no seu app Android

1. Antes de começar

Soluções tradicionais de autenticação apresentam uma série de desafios de segurança e usabilidade.

Senhas são amplamente usadas, mas…

  • facilmente esquecidas;
  • requerem conhecimento do usuário para criar senhas fortes;
  • são fáceis de ser descobertas por phishing, roubadas e reutilizadas por invasores.

O Android tem trabalhado na criação da API Credential Manager para simplificar a experiência de login e lidar com riscos de segurança ao oferecer chaves de acesso, o padrão do setor de última geração para autenticação sem senha.

O Credential Manager reúne a compatibilidade com chaves de acesso e a combina com métodos de autenticação tradicionais, como senhas, Fazer login com o Google etc.

Os usuários poderão criar e armazenar chaves de acesso no Gerenciador de senhas do Google, que as sincroniza entre os dispositivos Android em que o usuário fez login. Uma chave de acesso precisa ser criada, associada a uma conta de usuário e ter a chave pública armazenada em um servidor, antes que o usuário possa fazer login com ela.

Neste codelab, você vai aprender a se inscrever com chaves de acesso e senhas pela API Credential Manager e usar esses recursos para fins de autenticação futura. Existem dois fluxos:

  • Inscrição: com o uso de chaves de acesso e senha.
  • Login: com o uso de chaves de acesso e a senha salva.

Pré-requisitos

  • Conhecimentos básicos sobre como executar apps no Android Studio.
  • Conhecimentos básicos sobre o fluxo de autenticação em apps Android.
  • Conhecimentos básicos de chaves de acesso.

Conteúdo

  • Como criar uma chave de acesso.
  • Como salvar uma senha no gerenciador de senhas.
  • Como autenticar usuários com uma chave de acesso ou senha salva.

O que você vai precisar

Uma das seguintes combinações de dispositivos:

  • Um dispositivo Android com Android 9 ou mais recente (para chaves de acesso) e Android 4.4 ou mais recente (para autenticação por senha com a API Credential Manager).
  • De preferência, um dispositivo com sensor biométrico.
  • Não se esqueça de registrar uma biometria (ou bloqueio de tela).
  • Versão do plugin Kotlin: 1.8.10

2. Começar a configuração

  1. Clone este repo no laptop na ramificação credman_codelab: https://github.com/android/identity-samples/tree/credman_codelab.
  2. Acesse o módulo CredentialManager e abra o projeto no Android Studio.

Conferir o estado inicial do app

Para saber como funciona o estado inicial do app, siga estas etapas:

  1. Inicie o app.
  2. Você vai ver uma tela principal com um botão de inscrição e login.
  3. Clique na opção de inscrição para se cadastrar com uma chave de acesso ou senha.
  4. Clique no botão de login para acessar com a chave de acesso e a senha salva.

8c0019ff9011950a.jpeg

Para entender o que são e como funcionam as chaves de acesso, consulte Como as chaves de acesso funcionam?.

3. Adicionar a opção de se inscrever com chaves de acesso

Ao se inscrever para uma nova conta em um app Android que usa a API Credential Manager, é possível criar uma chave de acesso que fica armazenada com segurança no provedor de credenciais escolhido pelo usuário e é usada para logins futuros, o que elimina a necessidade de digitar senha todas as vezes.

Agora você vai criar uma chave de acesso e registrar as credenciais do usuário por biometria/bloqueio de tela.

Fazer a inscrição com chave de acesso

Em Credential Manager -> app -> main -> java -> SignUpFragment.kt, há um campo de texto "nome de usuário" e um botão para se inscrever com uma chave de acesso.

dcc5c529b310f2fb.jpeg

Transmitir o desafio e a outra resposta JSON para a chamada createPasskey()

Antes de criar uma chave de acesso, você precisa solicitar ao servidor as informações necessárias para serem transmitidas à API Credential Manager durante a chamada createCredential().

Por sorte, você já tem uma resposta simulada em assets(RegFromServer.txt) que retorna esses parâmetros neste codelab.

  • No app, acesse SignUpFragment.kt e encontre o método signUpWithPasskeys em que você vai escrever a lógica para criar uma chave de acesso e permitir a entrada do usuário. O método fica na mesma classe.
  • Marque o bloco else com um comentário para chamar createPasskey() e substitua pelo seguinte código:

SignUpFragment.kt

//TODO : Call createPasskey() to signup with passkey

val data = createPasskey()

Esse método será chamado assim que você tiver um nome de usuário válido preenchido na tela.

  • Dentro do método createPasskey(), você precisa criar um CreatePublicKeyCredentialRequest() com os parâmetros necessários retornados.

SignUpFragment.kt

//TODO create a CreatePublicKeyCredentialRequest() with necessary registration json from server

val request = CreatePublicKeyCredentialRequest(fetchRegistrationJsonFromServer())

O método fetchRegistrationJsonFromServer() lê a resposta JSON de inscrição dos ativos e retorna o JSON de inscrição a ser transmitido durante a criação da chave de acesso.

  • Encontre o método fetchRegistrationJsonFromServer() e substitua TODO pelo código a seguir para retornar o JSON. Além disso, remova a string vazia da instrução de retorno:

SignUpFragment.kt

//TODO fetch registration mock response

val response = requireContext().readFromAsset("RegFromServer")

//Update userId,challenge, name and Display name in the mock
return response.replace("<userId>", getEncodedUserId())
   .replace("<userName>", binding.username.text.toString())
   .replace("<userDisplayName>", binding.username.text.toString())
   .replace("<challenge>", getEncodedChallenge())
  • Aqui você lê o JSON de inscrição dos ativos.
  • Esse JSON tem quatro campos para serem substituídos.
  • UserId precisa ser exclusivo para que um usuário possa criar várias chaves de acesso (se necessário). Substitua <userId> pelo userId gerado.
  • <challenge> também precisa ser exclusivo, então você vai gerar um desafio aleatório e único. O método para isso já está implementado no código.

O snippet de código a seguir inclui exemplos de opções recebidas do servidor:

{
  "challenge": String,
  "rp": {
    "name": String,
    "id": String
  },
  "user": {
    "id": String,
    "name": String,
    "displayName": String
  },
  "pubKeyCredParams": [
    {
      "type": "public-key",
      "alg": -7
    },
    {
      "type": "public-key",
      "alg": -257
    }
  ],
  "timeout": 1800000,
  "attestation": "none",
  "excludeCredentials": [],
  "authenticatorSelection": {
    "authenticatorAttachment": "platform",
    "requireResidentKey": true,
    "residentKey": "required",
    "userVerification": "required"
  }
}

A tabela a seguir não é completa, mas tem os parâmetros importantes no dicionário PublicKeyCredentialCreationOptions:

Parâmetros

Descrições

challenge

Uma string aleatória gerada pelo servidor com entropia suficiente para tornar a adivinhação inviável. Precisa ter pelo menos 16 bytes de comprimento. É obrigatório, mas não usado durante a inscrição, a menos que a autenticação por atestado esteja ativada.

user.id

ID exclusivo de um usuário. Esse valor não pode incluir informações de identificação pessoal, por exemplo, endereços de e-mail ou nomes de usuário. Um valor aleatório de 16 bytes gerado por conta funciona bem.

user.name

Esse campo precisa conter um identificador exclusivo da conta que o usuário vai reconhecer, como endereço de e-mail ou nome de usuário, e vai ser exibido no seletor de contas. Se estiver usando um nome de usuário, use o mesmo valor da autenticação por senha.

user.displayName

Esse campo é um nome opcional e fácil de usar para a conta. É um apelido para a conta de usuário, destinado apenas para exibição.

rp.id

A Entidade de confiança representa os detalhes do app e precisa de:

  • Nome (obrigatório): nome do app.
  • ID (opcional): o domínio ou subdomínio do app. Se não for informado, o domínio atual vai ser usado.
  • Ícone (opcional).

pubKeyCredParams

Os Parâmetros de credencial de chave pública são uma lista de algoritmos e tipos de chave permitidos. Essa lista precisa conter pelo menos um elemento.

excludeCredentials

Ao tentar inscrever um novo dispositivo, é possível que o usuário já tenha registrado outros. Para evitar a criação de várias credenciais para a mesma conta em um único autenticador, ignore esses dispositivos. O membro transports, se informado, precisa conter o resultado da chamada de getTransports() durante o registro de cada credencial.

authenticatorSelection.authenticatorAttachment

Indica se o dispositivo precisa ser conectado à plataforma ou não, ou se não há alguma exigência sobre isso. Defina como "plataforma". Isso indica que você quer um autenticador incorporado no dispositivo da plataforma, e o usuário não precisará inserir, por exemplo, uma chave de segurança USB.

residentKey

Indica o valor "necessário" para criar uma chave de acesso.

Criar uma credencial

  1. Depois de criar CreatePublicKeyCredentialRequest(), faça uma chamada a createCredential() com a solicitação criada.

SignUpFragment.kt

//TODO call createCredential() with createPublicKeyCredentialRequest

try {
   response = credentialManager.createCredential(
       requireActivity(),
       request
   ) as CreatePublicKeyCredentialResponse
} catch (e: CreateCredentialException) {
   configureProgress(View.INVISIBLE)
   handlePasskeyFailure(e)
}
  • Você envia as informações necessárias para createCredential().
  • Depois que a solicitação for aceita, uma caixa de diálogo vai abrir na tela para você criar uma chave de acesso.
  • Agora os usuários podem confirmar a identidade por biometria ou bloqueio de tela etc.
  • Você lida com a visibilidade dos elementos renderizados e trata as exceções caso a solicitação falhe ou não dê certo por algum motivo. Aqui as mensagens de erro são registradas e mostradas no app em uma caixa de diálogo de erro. Confira os logs de erro completos no Android Studio ou pelo comando adb debug.

93022cb87c00f1fc.png

  1. Agora, por fim, você precisa concluir o processo de inscrição. Portanto, envie a credencial de chave pública ao servidor e deixe o usuário se conectar. O app recebe um objeto de credencial que tem uma chave pública e que pode ser enviada ao servidor para registrar a chave de acesso.

Aqui você usa um servidor simulado que retorna apenas true para indicar que salvou a chave pública registrada para fins de autenticação e validação futura.

No método signUpWithPasskeys(), encontre o comentário relevante e substitua pelo seguinte código:

SignUpFragment.kt

//TODO : complete the registration process after sending public key credential to your server and let the user in

data?.let {
   registerResponse()
   DataProvider.setSignedInThroughPasskeys(true)
   listener.showHome()
}
  • registerResponse retorna true, o que indica que o servidor (simulado) salvou a chave pública para uso futuro.
  • Defina SignedInThroughPasskeys como true para indicar que está se registrando com chaves de acesso.
  • Após o login, redirecione o usuário à tela inicial.

O snippet de código a seguir tem um exemplo das opções que você vai ter:

{
  "id": String,
  "rawId": String,
  "type": "public-key",
  "response": {
    "clientDataJSON": String,
    "attestationObject": String,
  }
}

A tabela seguinte não está completa, mas tem parâmetros importantes em PublicKeyCredential:

Parâmetros

Descrições

id

Um ID codificado em Base64URL da chave de acesso criada. Esse ID ajuda o navegador a determinar se uma chave de acesso correspondente está no dispositivo após a autenticação. Esse valor precisa ser armazenado no banco de dados no back-end.

rawId

Uma versão de objeto ArrayBuffer do ID da credencial.

response.clientDataJSON

Um objeto ArrayBuffer codificado com dados do cliente.

response.attestationObject

Um objeto de atestado codificado por ArrayBuffer. Ele tem informações importantes, como um ID da RP, sinalizações e uma chave pública.

Execute o app e você poderá clicar no botão de se inscrever com chaves de acesso e criar uma chave de acesso.

4. Salvar a senha no Provedor de credenciais

Na tela SignUp do app, você já tem a opção de se inscrever com nome de usuário e senha implementada para fins de demonstração.

Para salvar a credencial de senha do usuário no provedor de senhas, você vai implementar CreatePasswordRequest para transmitir a createCredential() e salvar a senha.

  • Encontre o método signUpWithPassword() e substitua TODO pela chamada createPassword:

SignUpFragment.kt

//TODO : Save the user credential password with their password provider

createPassword()
  • No método createPassword(), você precisa criar uma solicitação de senha como essa e substituir TODO por este código:

SignUpFragment.kt

//TODO : CreatePasswordRequest with entered username and password

val request = CreatePasswordRequest(
   binding.username.text.toString(),
   binding.password.text.toString()
)
  • Em seguida, no método createPassword(), você precisa criar uma credencial com a solicitação de criação de senha e salvar a credencial de senha do usuário no provedor de senhas. Substitua o TODO pelo seguinte código:

SignUpFragment.kt

//TODO : Create credential with created password request


try {
   credentialManager.createCredential(request, requireActivity()) as CreatePasswordResponse
} catch (e: Exception) {
   Log.e("Auth", " Exception Message : " + e.message)
}
  • Agora você salvou a credencial de senha no provedor de senhas do usuário para autenticação por senha com apenas um toque.

5. Adicionar a opção de autenticação com uma chave de acesso ou senha

Agora você está com tudo pronto para usar essa opção de autenticação no app com segurança.

629001f4a778d4fb.png

Acessar o desafio e outras opções para serem transmitidas à chamada getPasskey()

Antes de pedir ao usuário para se autenticar, você precisa solicitar parâmetros para transmitir o JSON WebAuthn do servidor, incluindo um desafio.

Você já tem uma resposta simulada em assets(AuthFromServer.txt) que retorna esses parâmetros neste codelab.

  • No app, acesse SignInFragment.kt e encontre o método signInWithSavedCredentials em que você vai escrever a lógica para autenticar com uma chave de acesso ou senha salva e permitir a entrada do usuário:
  • Marque o bloco else com um comentário para chamar createPasskey() e substitua pelo seguinte código:

SignInFragment.kt

//TODO : Call getSavedCredentials() method to signin using passkey/password

val data = getSavedCredentials()
  • No método getSavedCredentials(), você precisa criar um GetPublicKeyCredentialOption() com os parâmetros necessários para extrair as credenciais do provedor.

SignInFragment.kt

//TODO create a GetPublicKeyCredentialOption() with necessary registration json from server

val getPublicKeyCredentialOption =
   GetPublicKeyCredentialOption(fetchAuthJsonFromServer(), null)

fetchAuthJsonFromServer() é um método que lê a resposta JSON de autenticação dos ativos e retorna o JSON de autenticação para recuperar todas as chaves de acesso associadas à conta de usuário.

O segundo parâmetro, clientDataHash, é um hash usado para confirmar a identidade da parte de confiança e só vai ser definido se você tiver configurado GetCredentialRequest.origin. No caso do app de exemplo, vai ser nulo.

O terceiro parâmetro é true se você preferir que a operação retorne imediatamente quando não houver nenhuma credencial disponível, em vez de recorrer à descoberta de credenciais remotas, e false (padrão) caso contrário.

  • Encontre o método fetchAuthJsonFromServer() e substitua o TODO pelo seguinte código para retornar o JSON. Além disso, remova a string vazia da instrução de retorno:

SignInFragment.kt

//TODO fetch authentication mock json

return requireContext().readFromAsset("AuthFromServer")

Observação: o servidor deste codelab foi projetado para retornar um JSON o mais semelhante possível ao dicionário PublicKeyCredentialRequestOptions transmitido para a chamada à getCredential() da API. O snippet de código a seguir inclui alguns exemplos das opções que você vai ter:

{
  "challenge": String,
  "rpId": String,
  "userVerification": "",
  "timeout": 1800000
}

A tabela a seguir não é completa, mas tem os parâmetros importantes no dicionário PublicKeyCredentialRequestOptions:

Parâmetros

Descrições

challenge

Um desafio gerado pelo servidor em um objeto ArrayBuffer. Isso é necessário para evitar ataques repetidos. Nunca aceite o mesmo desafio em uma resposta duas vezes. Considere-o um token CSRF.

rpId

Um ID da RP é um domínio. Um site pode especificar o próprio domínio ou um sufixo registrável. Esse valor precisa corresponder ao parâmetro rp.id usado quando a chave de acesso foi criada.

  • Em seguida, você precisa criar um objeto PasswordOption() se quiser recuperar todas as senhas salvas no provedor pela API Credential Manager para esta conta de usuário. No método getSavedCredentials(), encontre e substitua TODO por:

SignInFragment.kt

//TODO create a PasswordOption to retrieve all the associated user's password

val getPasswordOption = GetPasswordOption()

Receber credenciais

  • Em seguida, chame a solicitação getCredential() com todas as opções acima para recuperar as credenciais associadas:

SignInFragment.kt

//TODO call getCredential() with required credential options

val result = try {
   credentialManager.getCredential(
       requireActivity(),
       GetCredentialRequest(
           listOf(
               getPublicKeyCredentialOption,
               getPasswordOption
           )  
     )
   )
} catch (e: Exception) {
   configureViews(View.INVISIBLE, true)
   Log.e("Auth", "getCredential failed with exception: " + e.message.toString())
   activity?.showErrorAlert(
       "An error occurred while authenticating through saved credentials. Check logs for additional details"
   )
   return null
}

if (result.credential is PublicKeyCredential) {
   val cred = result.credential as PublicKeyCredential
   DataProvider.setSignedInThroughPasskeys(true)
   return "Passkey: ${cred.authenticationResponseJson}"
}
if (result.credential is PasswordCredential) {
   val cred = result.credential as PasswordCredential
   DataProvider.setSignedInThroughPasskeys(false)
   return "Got Password - User:${cred.id} Password: ${cred.password}"
}
if (result.credential is CustomCredential) {
   //If you are also using any external sign-in libraries, parse them here with the utility functions provided.
}

  • Você precisa transmitir as informações necessárias para getCredential(). Isso inclui uma lista de opções de credenciais e um contexto de atividade para renderizar as opções em uma página inferior.
  • Se a solicitação for aceita, você vai ver uma caixa de diálogo na tela com todas as credenciais criadas para a conta associada.
  • Agora os usuários podem confirmar a identidade com biometria, bloqueio de tela etc. para autenticar a credencial escolhida.
  • Defina SignedInThroughPasskeys como true para indicar que está se registrando com chaves de acesso. Caso contrário, false.
  • Você lida com a visibilidade dos elementos renderizados e trata as exceções caso a solicitação falhe ou não dê certo por algum motivo. Aqui as mensagens de erro são registradas e mostradas no app em uma caixa de diálogo de erro. Confira os logs de erro completos no Android Studio ou pelo comando adb debug.
  • Agora, por fim, você precisa concluir o processo de inscrição. Portanto, envie a credencial de chave pública ao servidor e deixe o usuário se conectar. O app recebe um objeto de credencial que tem uma chave pública que pode ser enviada ao servidor para autenticação com a chave de acesso.

Aqui você usa um servidor simulado que retorna apenas true para indicar que validou a chave pública.

No método signInWithSavedCredentials(), encontre o comentário relevante e substitua pelo seguinte código:

SignInFragment.kt

//TODO : complete the authentication process after validating the public key credential to your server and let the user in.

data?.let {
   sendSignInResponseToServer()
   listener.showHome()
}
  • sendSigninResponseToServer() retorna true para indicar que o servidor (simulado) validou a chave pública para uso futuro.
  • Após o login, redirecione o usuário à tela inicial.

O snippet de código a seguir inclui um objeto PublicKeyCredential de exemplo:

{
  "id": String
  "rawId": String
  "type": "public-key",
  "response": {
    "clientDataJSON": String
    "authenticatorData": String
    "signature": String
    "userHandle": String
  }
}

A tabela a seguir não é completa, mas contém os parâmetros importantes no objeto PublicKeyCredential:

Parâmetros

Descrições

id

O ID codificado em Base64URL da credencial da chave de acesso autenticada.

rawId

Uma versão de objeto ArrayBuffer do ID da credencial.

response.clientDataJSON

Um objeto ArrayBuffer de dados do cliente. Esse campo contém informações, como o desafio e a origem que o servidor da RP precisa verificar.

response.authenticatorData

Um objeto ArrayBuffer de dados do autenticador. Esse campo contém informações como o ID da RP.

response.signature

Um objeto ArrayBuffer da assinatura. Esse valor é o núcleo da credencial e precisa ser verificado no servidor.

response.userHandle

Um objeto ArrayBuffer que contém o ID do usuário definido no momento da criação. Use esse valor em vez do ID da credencial se o servidor precisar escolher os valores do ID usado ou se o back-end quiser evitar a criação de um índice nos IDs das credenciais.

Abra o app, acesse "Fazer login" -> "Fazer login com chaves de acesso/senha salva" e tente entrar com as credenciais salvas.

Testar agora

Você implementou a criação de chaves de acesso, armazenamento de senhas no Credential Manager e autenticação por chaves de acesso ou senhas salvas com a API Credential Manager no app Android.

6. Parabéns!

Você concluiu este codelab. Se quiser conferir a resolução final, acesse https://github.com/android/identity-samples/tree/main/CredentialManager.

Se tiver dúvidas, envie para o StackOverflow com a tag passkey.

Saiba mais