Login com senha continua sendo uma das partes mais frágeis de muitos apps Android. O usuário esquece senha, reutiliza a mesma credencial em vários serviços, cai em phishing, abandona cadastro quando o fluxo pede etapas demais e ainda espera que a experiência funcione no celular com a mesma naturalidade de um app nativo. Em 2026, passkeys no Android com Kotlin viraram uma alternativa real para reduzir atrito e melhorar segurança sem obrigar todo produto a manter uma pilha complexa de autenticação própria.
Passkeys combinam criptografia de chave pública, WebAuthn/FIDO2, biometria ou bloqueio de tela do dispositivo e sincronização segura pelo provedor de credenciais. Para o usuário, a experiência parece simples: tocar em “entrar”, confirmar com impressão digital, rosto ou PIN e seguir. Para a engenharia, o ganho é maior: a aplicação deixa de enviar senha reutilizável e passa a trabalhar com desafios assinados, reduzindo risco de vazamento de senha e ataques de phishing.
Este guia mostra como pensar a implementação em Android usando Kotlin, Credential Manager, backend Kotlin e boas práticas de produto. A ideia não é transformar este artigo em uma especificação completa de WebAuthn, mas explicar o fluxo que uma equipe precisa dominar antes de colocar passkeys em produção. Se você já leu nosso conteúdo sobre segurança de dados locais no Android e Spring Security com Kotlin, este é um próximo passo natural.
O que são passkeys?
Uma passkey é uma credencial baseada em par de chaves criptográficas. A chave privada fica protegida no dispositivo ou no provedor de credenciais do usuário. A chave pública fica registrada no servidor. Quando o usuário tenta entrar, o servidor gera um desafio, o app pede ao Credential Manager para assinar esse desafio e o backend valida a assinatura usando a chave pública já cadastrada.
Essa diferença muda bastante a superfície de risco. Em um login com senha, o servidor recebe um segredo que o usuário conhece e provavelmente reutiliza. Em um login com passkey, o servidor recebe uma prova criptográfica daquele momento. Mesmo que alguém intercepte a resposta, ela não vira uma senha reutilizável para outro site.
Na prática, passkeys ajudam contra:
- reutilização de senhas vazadas;
- phishing em páginas falsas;
- ataques de força bruta contra login;
- atrito de recuperação de senha;
- credenciais fracas criadas apenas para concluir cadastro rápido.
Elas não eliminam toda responsabilidade de segurança. Você ainda precisa proteger sessão, tokens, recuperação de conta, dispositivos perdidos, logs e autorização. Passkey resolve autenticação primária, não toda a arquitetura de identidade.
Onde entra o Credential Manager?
No Android moderno, o caminho recomendado para trabalhar com passkeys é a biblioteca Credential Manager. Ela oferece uma API unificada para recuperar credenciais salvas, criar passkeys, usar senhas existentes quando necessário e integrar provedores de credenciais disponíveis no dispositivo.
Isso evita que o app implemente manualmente detalhes de UI e descoberta de credenciais. O Android apresenta a experiência apropriada, o usuário escolhe a credencial e o app recebe uma resposta para enviar ao backend.
Em um fluxo simplificado, o app Android faz quatro coisas:
- solicita ao backend as opções de criação ou autenticação;
- chama o Credential Manager com essas opções;
- recebe a resposta assinada pelo dispositivo/provedor;
- envia a resposta ao backend para verificação final.
O backend continua sendo decisivo. Ele precisa gerar desafios aleatórios, validar origem, validar rpId, armazenar chave pública, contar uso da credencial quando aplicável e emitir a sessão apenas depois da verificação.
Dependências básicas no Android
Em um projeto Android com Kotlin, a dependência principal fica no módulo do app. As versões mudam com o tempo, então trate o número abaixo como exemplo e confira a versão atual na documentação AndroidX antes de publicar.
dependencies {
implementation("androidx.credentials:credentials:1.3.0")
implementation("androidx.credentials:credentials-play-services-auth:1.3.0")
implementation("com.google.android.libraries.identity.googleid:googleid:1.1.1")
}
O credentials fornece a API central. O artefato de Play Services ajuda na compatibilidade com provedores Google em aparelhos Android. Dependendo do seu produto, você pode também aceitar senha tradicional, login federado ou outras credenciais no mesmo ponto de entrada.
Se o app já usa Kotlin coroutines, a integração fica mais natural, porque as chamadas modernas podem ser executadas em funções suspend e conectadas ao ViewModel sem bloquear a UI.
Criando uma passkey
O cadastro começa no servidor. O backend deve criar um desafio e retornar opções compatíveis com WebAuthn. Em muitos projetos, você usa uma biblioteca de WebAuthn no backend para montar esse JSON corretamente. O app Android não deve inventar challenge, user.id ou rp.id localmente.
Um exemplo conceitual no Android fica assim:
class PasskeyRepository(
private val credentialManager: CredentialManager,
private val authApi: AuthApi,
) {
suspend fun criarPasskey(context: Context): ResultadoLogin {
val optionsJson = authApi.iniciarCadastroPasskey()
val request = CreatePublicKeyCredentialRequest(
requestJson = optionsJson,
)
val response = credentialManager.createCredential(
context = context,
request = request,
) as CreatePublicKeyCredentialResponse
return authApi.finalizarCadastroPasskey(
credentialJson = response.registrationResponseJson,
)
}
}
O optionsJson geralmente contém dados como challenge, rp, user, algoritmos aceitos, tipo de autenticador e preferências de verificação do usuário. O registrationResponseJson volta com dados que o backend precisa validar e armazenar.
O detalhe importante é não tratar a criação da passkey como concluída apenas porque o Android retornou uma resposta. A credencial só deve ser considerada ativa depois que o backend validar a resposta e persistir a chave pública com segurança.
Entrando com uma passkey existente
O login segue uma ideia parecida, mas usa GetCredentialRequest com uma opção de chave pública. O backend primeiro gera um desafio de autenticação. O Android pede ao usuário para escolher ou confirmar a credencial. Depois, a resposta assinada volta para o servidor.
class LoginComPasskeyUseCase(
private val credentialManager: CredentialManager,
private val authApi: AuthApi,
) {
suspend fun entrar(context: Context): SessaoUsuario {
val optionsJson = authApi.iniciarLoginPasskey()
val request = GetCredentialRequest(
credentialOptions = listOf(
GetPublicKeyCredentialOption(
requestJson = optionsJson,
),
),
)
val result = credentialManager.getCredential(
context = context,
request = request,
)
val credential = result.credential as PublicKeyCredential
return authApi.finalizarLoginPasskey(
assertionJson = credential.authenticationResponseJson,
)
}
}
Em produção, trate exceções com cuidado. O usuário pode cancelar o fluxo, não ter credencial disponível, estar em um aparelho sem provedor compatível ou encontrar uma falha temporária. Cancelamento não é necessariamente erro de segurança; muitas vezes é apenas uma decisão do usuário. A UI deve oferecer alternativa clara, como login por e-mail, magic link ou senha, dependendo da estratégia do produto.
Como o backend Kotlin participa
O backend é a parte que mais precisa de disciplina. Em uma API Kotlin com Ktor ou Spring Boot, o fluxo de passkey normalmente tem quatro endpoints:
POST /auth/passkeys/registration/optionspara gerar opções de cadastro;POST /auth/passkeys/registration/verifypara validar e salvar a nova credencial;POST /auth/passkeys/authentication/optionspara gerar desafio de login;POST /auth/passkeys/authentication/verifypara validar assinatura e emitir sessão.
Um esboço com Ktor poderia ficar assim:
fun Route.passkeyRoutes(service: PasskeyService) {
route("/auth/passkeys") {
post("/registration/options") {
val user = call.principal<UsuarioPrincipal>()
call.respond(service.criarOpcoesCadastro(user.id))
}
post("/registration/verify") {
val payload = call.receive<PasskeyRegistrationPayload>()
val resultado = service.verificarCadastro(payload)
call.respond(resultado)
}
post("/authentication/options") {
val payload = call.receive<PasskeyLoginStartPayload>()
call.respond(service.criarOpcoesLogin(payload.email))
}
post("/authentication/verify") {
val payload = call.receive<PasskeyAssertionPayload>()
val sessao = service.verificarLogin(payload)
call.respond(sessao)
}
}
}
O PasskeyService deve delegar a validação WebAuthn para uma biblioteca confiável, não para código caseiro. WebAuthn tem detalhes de encoding, challenge, origem, contador de assinatura, algoritmos e propriedades do autenticador. Implementar isso manualmente aumenta muito o risco de aceitar uma assinatura inválida.
Se você usa Spring Boot, a mesma separação vale: controller fino, service com regra de identidade, repositório para credenciais e emissão de sessão isolada. O artigo de Spring Security com JWT e OAuth2 ajuda a pensar a etapa posterior: depois que a passkey autentica o usuário, como a API protege rotas e tokens.
Armazenamento de credenciais no servidor
Cada credencial precisa ser armazenada com metadados suficientes para auditoria e validação. Um modelo simplificado poderia incluir:
data class PasskeyCredential(
val id: String,
val userId: String,
val publicKeyCose: ByteArray,
val signCount: Long,
val transports: List<String>,
val createdAt: Instant,
val lastUsedAt: Instant?,
val label: String?,
)
Não salve apenas a chave pública e esqueça o resto. lastUsedAt ajuda suporte e segurança. label permite mostrar ao usuário algo como “Celular pessoal” ou “Notebook do trabalho”. transports pode ajudar diagnóstico. O contador de assinatura, quando usado, ajuda a detectar alguns cenários suspeitos, embora nem todo autenticador moderno se comporte da mesma forma.
Também vale permitir múltiplas passkeys por usuário. Uma pessoa pode usar celular, notebook e gerenciador de senhas. Forçar uma única credencial aumenta risco de bloqueio de conta.
UX: passkey não pode virar beco sem saída
O maior erro de produto é vender passkey como se ela eliminasse todos os fluxos alternativos. Dispositivos são perdidos, contas são restauradas, usuários trocam de ecossistema, provedores falham e empresas têm políticas diferentes para aparelhos corporativos.
Uma experiência saudável deve prever:
- cadastro de mais de uma passkey;
- nome amigável para credenciais cadastradas;
- remoção de credencial antiga;
- recuperação de conta com verificação forte;
- fallback documentado para suporte;
- comunicação clara quando o usuário cancela o prompt.
No Android, também tome cuidado com o momento de pedir a passkey. Se você coloca a criação no primeiro segundo do onboarding, antes de explicar valor, muita gente cancela. Um bom padrão é autenticar ou cadastrar a conta primeiro, mostrar o benefício e oferecer a criação da passkey como melhoria de segurança e conveniência.
Segurança local e sessão depois do login
Passkey autentica o usuário, mas o app ainda precisa guardar estado de sessão. Isso pode envolver access token curto, refresh token, cookies ou outro mecanismo definido pelo backend. A regra continua a mesma: tokens sensíveis não devem ficar em armazenamento local comum sem avaliação de risco.
Para apps Android, combine passkeys com práticas como:
- token de acesso curto;
- refresh token protegido com armazenamento adequado;
- limpeza de sessão no logout;
- logs sem tokens ou respostas WebAuthn completas;
- cuidado com backup automático de dados sensíveis;
- reautenticação para ações críticas.
Esse ponto conversa diretamente com segurança de dados locais no Android e com apps offline-first com Kotlin. Se o app permite ações offline, pense como elas se comportam quando a sessão expira, quando o usuário remove uma passkey ou quando o dispositivo volta de uma restauração de backup.
Testes que valem o esforço
Testar passkeys exige separar níveis. Você não precisa automatizar toda a biometria real em teste unitário, mas precisa testar regras de backend e estados de UI.
No Android, cubra pelo menos:
- usuário cancela criação da passkey;
- dispositivo sem credencial disponível;
- backend retorna desafio inválido ou expirado;
- login bem-sucedido emite sessão;
- falha de rede durante verificação;
- UI oferece fallback sem esconder o erro.
No backend, os testes mais importantes validam challenge único, expiração, origem esperada, persistência da credencial, emissão de sessão e rejeição de payload reaproveitado. Se o backend usa uma biblioteca WebAuthn, teste a integração ao redor dela e não duplique a suíte interna da biblioteca.
Para times que já usam JUnit e MockK, o desenho fica mais simples se o Credential Manager estiver atrás de uma interface própria. Assim, ViewModel e use cases podem receber respostas fake sem depender de prompt real do sistema.
interface PasskeyClient {
suspend fun criar(optionsJson: String): String
suspend fun autenticar(optionsJson: String): String
}
class LoginViewModel(
private val passkeyClient: PasskeyClient,
private val authApi: AuthApi,
) : ViewModel() {
fun loginComPasskey() = viewModelScope.launch {
val options = authApi.iniciarLoginPasskey()
val assertion = passkeyClient.autenticar(options)
authApi.finalizarLoginPasskey(assertion)
}
}
Essa abstração também ajuda a evitar que detalhes de Android vazem para domínio e repository. O código fica mais testável e mais claro para quem mantém a feature depois.
Erros comuns ao implementar passkeys
Alguns problemas aparecem com frequência em times que começam rápido demais:
- gerar challenge no app em vez do backend;
- não validar
originerpIdcorretamente; - guardar resposta WebAuthn completa em log;
- tratar cancelamento do usuário como erro fatal;
- não permitir múltiplas credenciais por conta;
- criar fallback inseguro que enfraquece todo o ganho da passkey;
- emitir sessão antes da verificação final do backend;
- não documentar recuperação de conta.
O último ponto é importante. Se a recuperação de conta permite tomar posse com um simples link de e-mail sem proteção adicional, a passkey deixa de ser o elo forte do fluxo. Segurança é sempre o conjunto, não apenas a tecnologia mais moderna da tela de login.
Quando adotar passkeys em um app Kotlin?
Passkeys fazem mais sentido quando o app tem login recorrente, dados sensíveis, risco de phishing, abandono por senha ou base de usuários já acostumada com biometria no celular. Fintechs, saúde, educação, apps corporativos, e-commerce, SaaS mobile e produtos com autenticação frequente tendem a se beneficiar bastante.
Para um MVP simples, talvez seja melhor começar com login federado ou magic link e planejar passkeys quando a identidade virar gargalo. Para um produto estabelecido, a adoção incremental costuma ser a melhor estratégia: permitir criar passkey depois do login, medir taxa de adesão, acompanhar falhas e só então promover o fluxo como opção principal.
Também existe uma oportunidade de carreira. Desenvolvedores Android e backend que entendem passkeys, Credential Manager, WebAuthn e segurança de sessão demonstram maturidade além do CRUD. Em vagas Kotlin, esse conhecimento conversa com Android moderno, arquitetura de autenticação e integração segura com APIs.
Conclusão
Passkeys no Android com Kotlin são uma evolução prática, não apenas uma tendência de segurança. Elas reduzem dependência de senhas, melhoram a experiência do usuário e aproximam apps brasileiros de um padrão de autenticação mais resistente a phishing. Mas a implementação correta exige cooperação entre Android, backend, produto e suporte.
Use Credential Manager para a experiência nativa, deixe o backend gerar e validar desafios, armazene credenciais com metadados, teste cancelamentos e falhas, e mantenha uma estratégia segura de recuperação de conta. Depois do login, continue tratando sessão, tokens e dados locais com a mesma disciplina.
Se o seu time está modernizando autenticação em Kotlin, passkeys merecem entrar no roadmap ao lado de Spring Security, Ktor para APIs e boas práticas de Android seguro. Para comparar com outro ecossistema backend usado em autenticação e APIs de alta escala, vale acompanhar também conteúdos de Go para serviços web no Golang Brasil.