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
- Clone este repo no laptop na ramificação credman_codelab: https://github.com/android/identity-samples/tree/credman_codelab.
- 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:
- Inicie o app.
- Você vai ver uma tela principal com um botão de inscrição e login.
- Clique na opção de inscrição para se cadastrar com uma chave de acesso ou senha.
- Clique no botão de login para acessar com a chave de acesso e a senha salva.
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.
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 |
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. | |
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. | |
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. | |
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. | |
A Entidade de confiança representa os detalhes do app e precisa de:
| |
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. | |
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. | |
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. | |
| Indica o valor "necessário" para criar uma chave de acesso. |
Criar uma credencial
- 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.
- 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 |
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. | |
Uma versão de objeto | |
Um objeto | |
Um objeto de atestado codificado por |
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.
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 |
Um desafio gerado pelo servidor em um objeto | |
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 |
- 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 |
O ID codificado em Base64URL da credencial da chave de acesso autenticada. | |
Uma versão de objeto | |
Um objeto | |
Um objeto | |
Um objeto | |
Um objeto |
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
.