Como implementar Zero-Tap Sign-In no Android com a Restore Credentials API: guia completo
A partir de abril de 2027, o Google Play exigirá que todos os apps com sistema de login implementem o Zero-Tap Sign-In — a restauração automática de credenciais quando o usuário migra para um novo dispositivo Android. Apps que não cumprirem este requisito terão visibilidade reduzida e restrições de publicação na Play Store.
Este tutorial técnico mostra, passo a passo, como implementar a Restore Credentials API usando o Credential Manager do Jetpack. Cobrimos desde a configuração do projeto até o tratamento de edge cases, com código Kotlin completo e testável. Jogos estão isentos desta exigência — o tutorial é voltado para apps convencionais.
O que é o Zero-Tap Sign-In e por que ele é obrigatório
Quando um usuário compra um novo smartphone Android e configura o dispositivo restaurando do backup (cloud ou device-to-device), ele espera que seus apps funcionem exatamente como no dispositivo anterior. Historicamente, isso não acontecia: cada app exigia que o usuário fizesse login novamente, digitando e-mail e senha ou passando por fluxos de OAuth.
O Zero-Tap Sign-In resolve esse problema. A Restore Credentials API cria uma chave criptográfica chamada restore key — similar a uma passkey — que é armazenada localmente e/ou no backup criptografado do Google. Quando o usuário configura o novo dispositivo, a restore key é transferida automaticamente. No primeiro lançamento do app no novo dispositivo, o Credential Manager recupera a restore key silenciosamente e o app pode autenticar o usuário sem nenhuma interação.
Requisitos mínimos
A Restore Credentials API funciona em: dispositivos com Android 9 (API 28) ou superior, Google Play Services core versão 24220000 ou superior, e androidx.credentials versão 1.5.0 ou superior (recomendamos usar a versão mais recente). A API funciona independentemente de o app já suportar passkeys, Google Sign-In, ou login com senha — ela opera como uma camada complementar ao mecanismo de autenticação existente.
Passo 1: Configurar o projeto
Adicionar dependências
// build.gradle.kts (módulo app)
dependencies {
implementation("androidx.credentials:credentials:1.7.0-alpha03")
implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha03")
}Configurar o servidor (Relying Party)
A Restore Credentials API usa o mesmo protocolo WebAuthn das passkeys. Isso significa que você precisa de um servidor capaz de processar PublicKeyCredentialCreationOptions e validar asserções. Se o seu app já suporta passkeys, o mesmo servidor pode ser reutilizado para restore keys sem modificação.
Se o app ainda não tem suporte a passkeys, será necessário configurar um servidor FIDO-compliant. O Google oferece documentação detalhada sobre a implementação server-side em sua guia de passkeys.
// Exemplo da resposta do servidor para criação de restore key
// Formato PublicKeyCredentialCreationOptionsJSON
{
"challenge": "base64url-encoded-challenge",
"rp": {
"name": "Seu App",
"id": "seudominio.com"
},
"user": {
"id": "base64url-encoded-user-id",
"name": "usuario@email.com",
"displayName": "Nome do Usuário"
},
"pubKeyCredParams": [
{ "type": "public-key", "alg": -7 },
{ "type": "public-key", "alg": -257 }
],
"timeout": 60000,
"attestation": "none",
"authenticatorSelection": {
"residentKey": "required",
"userVerification": "discouraged"
}
}Passo 2: Criar a Restore Key
A restore key deve ser criada sempre que o usuário estiver autenticado. Os cenários ideais são: após o login bem-sucedido, após a criação de conta, e no onCreate da Activity principal (se o usuário já está logado e ainda não tem restore key).
Instanciar o Credential Manager
import androidx.credentials.CredentialManager
import androidx.credentials.CreateRestoreCredentialRequest
import androidx.credentials.exceptions.restorecredential.CreateRestoreCredentialDomException
import androidx.credentials.exceptions.restorecredential.E2eeUnavailableException
class RestoreCredentialsManager(private val context: Context) {
private val credentialManager = CredentialManager.create(context)
private val prefs = context.getSharedPreferences("restore_creds", Context.MODE_PRIVATE)
private fun hasRestoredCredential(): Boolean {
return prefs.getBoolean("has_synced_restore_credential", false)
}
private fun markRestoreCredentialSynced() {
prefs.edit().putBoolean("has_synced_restore_credential", true).apply()
}
}Implementar a criação da restore key
suspend fun createRestoreKey(activity: Activity) {
if (hasRestoredCredential()) {
Log.d("RestoreCreds", "Restore key já existe — ignorando criação")
return
}
try {
val creationOptionsJson = fetchCreationOptionsFromServer()
val createRequest = CreateRestoreCredentialRequest(
requestJson = creationOptionsJson,
isCloudBackupEnabled = true // Recomendado
)
val response = credentialManager.createCredential(
context = activity,
request = createRequest
)
sendPublicKeyToServer(response.registrationResponseJson)
markRestoreCredentialSynced()
Log.d("RestoreCreds", "Restore key criada com sucesso")
} catch (e: E2eeUnavailableException) {
// Dispositivo sem E2EE — tentar sem cloud backup
Log.w("RestoreCreds", "E2EE indisponível — criando restore key local")
createRestoreKeyWithoutCloud(activity)
} catch (e: CreateRestoreCredentialDomException) {
Log.e("RestoreCreds", "requestJson inválido: ${e.message}")
} catch (e: IllegalArgumentException) {
Log.e("RestoreCreds", "Argumento inválido: ${e.message}")
} catch (e: Exception) {
Log.e("RestoreCreds", "Erro ao criar restore key: ${e.message}")
}
}
private suspend fun createRestoreKeyWithoutCloud(activity: Activity) {
try {
val creationOptionsJson = fetchCreationOptionsFromServer()
val createRequest = CreateRestoreCredentialRequest(
requestJson = creationOptionsJson,
isCloudBackupEnabled = false
)
val response = credentialManager.createCredential(
context = activity,
request = createRequest
)
sendPublicKeyToServer(response.registrationResponseJson)
markRestoreCredentialSynced()
} catch (e: Exception) {
Log.e("RestoreCreds", "Falha ao criar restore key local: ${e.message}")
}
}Integrar no fluxo de login
class LoginViewModel(
private val authRepository: AuthRepository,
private val restoreCredentialsManager: RestoreCredentialsManager
) : ViewModel() {
fun onLoginSuccess(activity: Activity) {
viewModelScope.launch {
try {
restoreCredentialsManager.createRestoreKey(activity)
} catch (e: Exception) {
Log.e("Login", "Falha ao criar restore key: ${e.message}")
}
}
}
}
// Na MainActivity — verificar se usuário logado tem restore key
class MainActivity : AppCompatActivity() {
private lateinit var restoreCredentialsManager: RestoreCredentialsManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
restoreCredentialsManager = RestoreCredentialsManager(this)
if (authRepository.isUserLoggedIn()) {
lifecycleScope.launch {
restoreCredentialsManager.createRestoreKey(this@MainActivity)
}
}
}
}Passo 3: Recuperar a Restore Key no novo dispositivo
Quando o usuário configura um novo dispositivo e abre o app pela primeira vez, o Credential Manager deve ser consultado para obter a restore key. Este processo é silencioso — não exibe nenhuma interface ao usuário.
Implementar a recuperação
suspend fun signInWithRestoreKey(activity: Activity): Boolean {
try {
val authenticationJson = fetchAuthenticationOptionsFromServer()
val options = GetRestoreCredentialOption(authenticationJson)
val getRequest = GetCredentialRequest(listOf(options))
val response = credentialManager.getCredential(
context = activity,
request = getRequest
)
val credential = response.credential
if (credential is RestoreCredential) {
val authResult = authenticateWithServer(
credential.authenticationResponseJson
)
if (authResult.isSuccess) {
Log.d("RestoreCreds", "Login restaurado via restore key")
markRestoreCredentialSynced()
return true
}
}
return false
} catch (e: NoCredentialException) {
Log.d("RestoreCreds", "Nenhuma restore key disponível")
return false
} catch (e: Exception) {
Log.e("RestoreCreds", "Erro ao recuperar restore key: ${e.message}")
return false
}
}Integrar no fluxo de inicialização do app
class SplashActivity : AppCompatActivity() {
private lateinit var restoreCredentialsManager: RestoreCredentialsManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
restoreCredentialsManager = RestoreCredentialsManager(this)
lifecycleScope.launch {
val destination = determineDestination()
startActivity(Intent(this@SplashActivity, destination))
finish()
}
}
private suspend fun determineDestination(): Class<*> {
if (authRepository.isUserLoggedIn()) {
return MainActivity::class.java
}
val restored = restoreCredentialsManager.signInWithRestoreKey(this)
if (restored) {
return MainActivity::class.java
}
return LoginActivity::class.java
}
}Recuperação via BackupAgent (cenário avançado)
Para apps de mensagens e comunicação, é recomendável recuperar a restore key assim que o backup do app for restaurado — antes mesmo do usuário abrir o app. Isso permite enviar notificações imediatamente no novo dispositivo.
class CustomBackupAgent : BackupAgent() {
override fun onRestoreFinished() {
super.onRestoreFinished()
// Restaurar credenciais assim que o backup for concluído
CoroutineScope(Dispatchers.IO).launch {
try {
val credentialManager = CredentialManager.create(applicationContext)
val authJson = fetchAuthenticationOptionsFromServer()
val options = GetRestoreCredentialOption(authJson)
val getRequest = GetCredentialRequest(listOf(options))
val response = credentialManager.getCredential(
context = applicationContext,
request = getRequest
)
val credential = response.credential as? RestoreCredential
credential?.let {
authenticateWithServer(it.authenticationResponseJson)
// Reconfigurar FCM para notificações
FirebaseMessaging.getInstance().token.addOnSuccessListener { token ->
sendFcmTokenToServer(token)
}
}
} catch (e: Exception) {
Log.e("Backup", "Falha ao restaurar credenciais: ${e.message}")
}
}
}
// IMPORTANTE: usar onRestoreFinished(), NÃO onRestore()
// onRestoreFinished() é chamado para qualquer tipo de restore
override fun onBackup(oldState: ParcelFileDescriptor?, data: BackupDataOutput?, newState: ParcelFileDescriptor?) {}
override fun onRestore(data: BackupDataInput?, appVersionCode: Int, newState: ParcelFileDescriptor?) {}
}Passo 4: Deletar a Restore Key no logout
Por segurança, a restore key deve ser deletada sempre que o usuário faz logout. Isso impede que um futuro dono do dispositivo seja automaticamente logado na conta do usuário anterior.
suspend fun deleteRestoreKey() {
try {
val clearRequest = ClearCredentialStateRequest(
ClearCredentialStateRequest.TYPE_CLEAR_RESTORE_CREDENTIAL
)
credentialManager.clearCredentialState(clearRequest)
prefs.edit().putBoolean("has_synced_restore_credential", false).apply()
Log.d("RestoreCreds", "Restore key deletada com sucesso")
} catch (e: Exception) {
Log.e("RestoreCreds", "Erro ao deletar restore key: ${e.message}")
}
}
// Integrar no fluxo de logout
class SettingsViewModel(
private val authRepository: AuthRepository,
private val restoreCredentialsManager: RestoreCredentialsManager
) : ViewModel() {
fun onLogout() {
viewModelScope.launch {
restoreCredentialsManager.deleteRestoreKey()
authRepository.clearSession()
_navigateToLogin.emit(true)
}
}
}Passo 5: Testar a implementação
O Android oferece ferramentas específicas para testar a Restore Credentials sem precisar de dois dispositivos físicos.
Teste via ADB
# Simular backup e restore para testar o fluxo completo
# 1. Instalar o app e fazer login (isso cria a restore key)
adb install app-debug.apk
# 2. Fazer backup do app
adb shell bmgr backupnow com.seuapp.package
# 3. Desinstalar o app (simula troca de dispositivo)
adb uninstall com.seuapp.package
# 4. Reinstalar o app
adb install app-debug.apk
# 5. Restaurar o backup
adb shell bmgr restore com.seuapp.package
# 6. Abrir o app — deve fazer login automaticamente
adb shell am start -n com.seuapp.package/.SplashActivityTeste com device-to-device transfer
Para testar a transferência device-to-device, use dois dispositivos físicos ou emuladores. Configure o backup no dispositivo de origem, e no dispositivo de destino, escolha “Restaurar de outro dispositivo” durante o setup wizard.
Verificação de edge cases
@RunWith(AndroidJUnit4::class)
class RestoreCredentialsTest {
@Test
fun testCreateRestoreKey_userLoggedIn_success() = runTest {
val manager = RestoreCredentialsManager(context)
manager.createRestoreKey(activity)
assertTrue(manager.hasRestoredCredential())
}
@Test
fun testCreateRestoreKey_alreadyExists_skips() = runTest {
val manager = RestoreCredentialsManager(context)
manager.createRestoreKey(activity) // Primeira vez
manager.createRestoreKey(activity) // Deve pular
}
@Test
fun testSignInWithRestoreKey_noCredential_returnsFalse() = runTest {
val manager = RestoreCredentialsManager(context)
val result = manager.signInWithRestoreKey(activity)
assertFalse(result)
}
@Test
fun testDeleteRestoreKey_onLogout_clearsState() = runTest {
val manager = RestoreCredentialsManager(context)
manager.createRestoreKey(activity)
manager.deleteRestoreKey()
assertFalse(manager.hasRestoredCredential())
}
}Passo 6: Migrar de mecanismos legados
Se o seu app ainda usa Smart Lock for Passwords ou a antiga Google Sign-In API, será necessário migrar para o Credential Manager antes de implementar Restore Credentials.
Migração do Smart Lock for Passwords
// ANTES (Smart Lock - legado)
val credentialsClient = Credentials.getClient(activity)
val request = CredentialRequest.Builder()
.setPasswordLoginSupported(true)
.build()
credentialsClient.request(request)
// DEPOIS (Credential Manager - moderno)
val credentialManager = CredentialManager.create(context)
val getPasswordOption = GetPasswordOption()
val getRequest = GetCredentialRequest(listOf(getPasswordOption))
val response = credentialManager.getCredential(context, getRequest)Migração do FIDO2
// ANTES (FIDO2 API - legado)
val fido2ApiClient = Fido2ApiClient(activity)
val options = PublicKeyCredentialRequestOptions.Builder()
.setChallenge(challenge)
.setRpId("seudominio.com")
.build()
// DEPOIS (Credential Manager com passkeys)
val credentialManager = CredentialManager.create(context)
val getPublicKeyCredOption = GetPublicKeyCredentialOption(requestJson)
val getRequest = GetCredentialRequest(listOf(getPublicKeyCredOption))
val response = credentialManager.getCredential(context, getRequest)A documentação oficial do Android oferece guias detalhados para cada cenário de migração, incluindo compatibilidade reversa para dispositivos que ainda não suportam o Credential Manager.
Arquitetura recomendada
Para apps de médio e grande porte, recomendamos encapsular toda a lógica de Restore Credentials em um módulo separado com interface limpa:
interface RestoreCredentialService {
suspend fun createRestoreKey(activity: Activity): Result
suspend fun tryRestoreSignIn(activity: Activity): Result
suspend fun clearRestoreKey(): Result
fun hasRestoreKey(): Boolean
}
// Implementação com injeção de dependência (Hilt)
@Module
@InstallIn(SingletonComponent::class)
object RestoreCredentialsModule {
@Provides
@Singleton
fun provideRestoreCredentialService(
@ApplicationContext context: Context,
authApi: AuthApi
): RestoreCredentialService {
return RestoreCredentialServiceImpl(context, authApi)
}
} Como a Mind Group pode ajudar na implementação
Na Mind Group, temos experiência prática na implementação de autenticação moderna em apps Android. Como software house especializada em desenvolvimento sob medida, nossa equipe em São Paulo e Sorocaba já realizou migrações de Smart Lock e FIDO2 para Credential Manager em projetos de clientes de diversos segmentos.
Nosso processo inclui: auditoria do mecanismo de autenticação atual, planejamento da migração para Credential Manager, implementação da Restore Credentials API com testes automatizados, configuração do servidor FIDO-compliant (quando necessário), e validação completa do fluxo de backup e restore.
Se o seu app precisa implementar Zero-Tap Sign-In antes do prazo de abril de 2027, entre em contato com a Mind Group para uma avaliação técnica personalizada.
Perguntas frequentes sobre Zero-Tap Sign-In e Restore Credentials
Meu app já usa passkeys. Preciso fazer algo diferente para restore credentials?
Não muito. Como restore keys usam o mesmo protocolo WebAuthn das passkeys, o seu servidor já está preparado. No lado do cliente, você só precisa adicionar a criação de CreateRestoreCredentialRequest no fluxo de login e a recuperação via GetRestoreCredentialOption na inicialização do app.
A restore key é transferida automaticamente entre dispositivos?
Sim. A transferência da restore key está integrada ao mecanismo de backup e restore do Android — seja via cloud backup ou device-to-device transfer. O desenvolvedor não precisa implementar nenhuma lógica de transferência.
O que acontece se o usuário não tem tela de bloqueio configurada?
Sem tela de bloqueio, o dispositivo não suporta criptografia de ponta a ponta (E2EE), e a API lança E2eeUnavailableException. A solução é capturar essa exceção e criar a restore key com isCloudBackupEnabled = false, armazenando apenas localmente.
Restore credentials funcionam com qualquer método de autenticação?
Sim. A Restore Credentials API funciona independentemente do método de login do app — seja senha, Google Sign-In, passkeys, ou login social. Ela opera como uma camada complementar que não interfere no mecanismo de autenticação existente.
Como diferenciar restore keys de passkeys no servidor?
Embora usem o mesmo protocolo server-side, é importante marcar restore keys como tal no banco de dados. Restore keys são gerenciadas pelo sistema e não devem aparecer na página de gerenciamento de passkeys do usuário. Use um campo credential_type com valores como "passkey" e "restore_key".
Preciso implementar Zero-Tap Sign-In para jogos?
Não. O Google isentou jogos da exigência de Zero-Tap Sign-In prevista para abril de 2027. Porém, jogos ainda precisam cumprir os requisitos de memória e otimização de código DEX que entram em vigor em fevereiro de 2027.
