Guia de Implantação de Biometria em Projetos Flutter + Supabase
Índice
- 1. 1. Dependências
- 1.1. 1.1 local_auth
- 1.1.1. Instalação
- 1.1.2. Plataformas suportadas
- 1.2. 1.2 flutter_secure_storage
- 1.2.1. Instalação
- 1.2.2. Plataformas suportadas
- 1.2.3. Uso típico
- 1.3. 1.3 Dependências secundárias e ajustes
- 1.4. 1.4 Observação Importante (Supabase)
- 2. 2. Ajustes de Plataforma
- 2.1. 2.1 Android
- 2.1.1. 2.1.1 Alteração em MainActivity
- 2.1.1.1. Solução
- 2.1.2. 2.1.2 Permissões no AndroidManifest.xml
- 2.1.3. 2.1.3 Compatibilidade de SDK
- 2.1.4. 2.1.4 Testes em dispositivos/emuladores
- 2.2. Resumindo:
- 3. 3. Service Layer
- 3.1. 3.1 BiometricService
- 3.1.1. Função
- 3.1.2. Métodos principais
- 3.1.3. Exemplo simplificado
- 3.2. 3.2 SecureStorageService
- 3.2.1. Função
- 3.2.2. Métodos principais
- 3.2.3. Exemplo simplificado
- 3.3. 3.3 Integração Biometria + Storage
- 4. 3.4 Resumo
- 5. 4. Repository Layer
- 5.1. 4.1 Interface IBiometricRepository
- 5.2. 4.2 Implementação BiometricRepository
- 5.3. 4.3 Responsabilidades de cada método
- 5.4. 4.4 Ponto importante (limitação Supabase)
- 6. 4.5 Resumo
- 7. 5. Use Case / AuthService
- 7.1. 5.1 Situação atual do Supabase (GoTrue v2, 2025)
- 7.2. 5.2 Implicação no login biométrico
- 7.3. 5.3 Solução implementada
- 7.3.1. Fluxo de login inicial (email/senha)
- 7.3.2. Fluxo de login com biometria (segundo boot em diante)
- 7.4. 5.4 Vantagens dessa abordagem
- 7.5. 5.5 Limitações
- 8. 5.6 Resumo
- 9. 6. UI / ViewModel
- 9.1. 6.1 ViewModel
- 9.2. 6.2 UI
- 9.3. 6.3 Resumo
- 10. 7. Conclusões
Neste guia documento a implantação de Biometria em um projeto pessoal, registrando os detalhes técnicos e as decisões tomadas durante o processo.
Esta documentação foi produzida com o auxílio do ChatGPT, a partir das alterações realizadas no código do projeto, de discussões sobre o uso de biometria com o Supabase e de consultas à documentação oficial das ferramentas envolvidas.
O objetivo é agrupar as informações relevantes a esta implementação em um único material de referência, servindo como guia para futuras reproduções ou adaptações da funcionalidade em outros projetos.
O projeto segue uma arquitetura em camadas baseada no padrão MVVM (Model–View–ViewModel), alinhada às recomendações do guia oficial de arquitetura do Flutter Team: Flutter App Architecture Guide. Essa escolha permite separar claramente infraestrutura, lógica de negócio e interface, tornando o código mais modular, testável e adaptável para futuras evoluções.
1. Dependências
A implantação da biometria no Sports se baseia em duas bibliotecas principais do Flutter:
1.1 local_auth
- Função: fornece a interface nativa para autenticação biométrica no Android (impressão digital, reconhecimento facial) e iOS (FaceID, TouchID).
- Porque usar:
- É o pacote oficial do Flutter para biometria.
- Abstrai as diferenças entre plataformas.
- Limitações:
- Apenas informa se a autenticação foi bem-sucedida ou não.
- Não entrega um token ou identidade — só libera o acesso.
- Exige que a Activity do Android seja
FlutterFragmentActivity
.
Instalação
dependencies: local_auth: ^2.3.0
Plataformas suportadas
- Android → Impressão digital, Face Unlock (quando disponível), Iris.
- iOS → Face ID, Touch ID.
- Windows → suporte experimental.
1.2 flutter_secure_storage
- Função: provê armazenamento seguro de dados no dispositivo, usando:
- Keychain (iOS).
- Keystore (Android).
- Porque usar:
- Armazena credenciais ou tokens localmente, de forma criptografada.
- É a única forma confiável de manter dados sensíveis entre sessões.
Instalação
dependencies: flutter_secure_storage: ^4.2.1
Plataformas suportadas
- Android, iOS, Linux, macOS, Windows.
Uso típico
- Salvar credenciais do usuário após login com email/senha.
- Recuperar essas credenciais após autenticação biométrica.
1.3 Dependências secundárias e ajustes
Além das duas principais, foram necessários alguns ajustes adicionais:
- Android
- Alterar
MainActivity
para herdar deFlutterFragmentActivity
:class MainActivity : FlutterFragmentActivity()
- Sem isso,
local_auth
falha com:PlatformException(no_fragment_activity, local_auth plugin requires activity to be a FragmentActivity.)
- Linux
flutter_secure_storage
precisa estar registrado em:linux/flutter/generated_plugin_registrant.cc
linux/flutter/generated_plugins.cmake
- Flutter SDK
- O
local_auth
exige SDK mínimo atualizado. No Sports, oflutter
foi ajustado para>=3.35.0
.
1.4 Observação Importante (Supabase)
O Supabase atualmente não fornece refresh tokens reutilizáveis (GoTrue v2).
- O campo
session.refreshToken
retorna apenas um identificador curto (abc123xyz
), que não pode ser usado emsetSession()
. - Por isso, optou-se por armazenar as credenciais do usuário (email/senha) em
flutter_secure_storage
, protegidas pela biometria. - No login biométrico → desbloqueia as credenciais → executa novamente
signInWithPassword()
. - Ou seja: biometria hoje funciona como atalho seguro para login com credenciais, não como restauração de sessão via refresh token.
2. Ajustes de Plataforma
2.1 Android
2.1.1 Alteração em MainActivity
O plugin local_auth
depende do uso de FragmentActivity, pois a autenticação biométrica precisa de um fragment manager para lidar com os diálogos nativos do sistema.
Por padrão, o Flutter gera uma MainActivity
que estende FlutterActivity
. Isso causa o erro:
PlatformException(no_fragment_activity, local_auth plugin requires activity to be a FragmentActivity., null, null)
Solução
Editar o arquivo:android/app/src/main/kotlin/<seu_pacote>/MainActivity.kt
Trocar:
import io.flutter.embedding.android.FlutterActivity class MainActivity: FlutterActivity() { }
Por:
import io.flutter.embedding.android.FlutterFragmentActivity class MainActivity : FlutterFragmentActivity()
Caso o projeto esteja em Java, o mesmo se aplica:
import io.flutter.embedding.android.FlutterFragmentActivity; public class MainActivity extends FlutterFragmentActivity { }
2.1.2 Permissões no AndroidManifest.xml
O local_auth
geralmente não exige permissões extras, pois usa o sistema nativo.
Mas em versões antigas de Android, pode ser necessário garantir:
<uses-permission android:name="android.permission.USE_BIOMETRIC"/> <uses-permission android:name="android.permission.USE_FINGERPRINT"/>
Nota:
USE_FINGERPRINT
é legado, mas ainda útil para Android < 10.
Em versões recentes, apenasUSE_BIOMETRIC
é suficiente.
2.1.3 Compatibilidade de SDK
- No
android/app/build.gradle.kts
, certifique-se de usar:
defaultConfig { minSdk = 23 // obrigatório para local_auth targetSdk = 34 }
2.1.4 Testes em dispositivos/emuladores
- Emuladores não suportam biometria por padrão.
- Para testar:
- Use
adb
para simular toque de digital:adb -e emu finger touch 1
- Ou habilite Fingerprint no AVD Manager.
- Em dispositivos reais, basta cadastrar digital/face nas configurações do sistema.
Resumindo:
- Alterar
MainActivity
paraFlutterFragmentActivity
. - Garantir permissões (
USE_BIOMETRIC
,USE_FINGERPRINT
). - Usar
minSdk >= 23
. - Testar em dispositivo físico para validar fluxo.
3. Service Layer
A camada de Service é responsável por lidar diretamente com as bibliotecas externas (local_auth
, flutter_secure_storage
) e expor uma API simples e consistente para o restante da aplicação.
Ela não deve conter lógica de negócio, apenas adaptação entre o app e o SDK/plataforma.
3.1 BiometricService
Função
Encapsular a interação com o pacote local_auth
.
Isso permite que o restante da aplicação não dependa diretamente do plugin.
Métodos principais
isAvailable()
- Verifica se o dispositivo suporta biometria e se há alguma biometria cadastrada.
- Combina
canCheckBiometrics
eisDeviceSupported
. authenticate(reason)
- Exibe o prompt nativo de autenticação biométrica.
- Retorna
true
/false
de acordo com o sucesso ou falha. - Recebe o parâmetro
reason
, que é exibido no diálogo nativo (ex.: “Use sua digital para continuar”).
Exemplo simplificado
import 'package:local_auth/local_auth.dart'; class BiometricService { final LocalAuthentication _auth = LocalAuthentication(); Future<bool> isAvailable() async { try { final canCheck = await _auth.canCheckBiometrics; final supported = await _auth.isDeviceSupported(); return canCheck && supported; } catch (_) { return false; } } Future<bool> authenticate({String reason = 'Authenticate to continue'}) async { try { return await _auth.authenticate( localizedReason: reason, options: const AuthenticationOptions( biometricOnly: true, stickyAuth: true, ), ); } catch (_) { return false; } } }
3.2 SecureStorageService
Função
Gerenciar o armazenamento seguro de informações sensíveis no dispositivo, usando flutter_secure_storage
.
É aqui que ficam guardadas as credenciais (no caso atual, email
e senha
, já que o Supabase não disponibiliza refresh tokens reutilizáveis).
Métodos principais
saveString(key, value)
- Salva dados criptografados no Keychain (iOS) ou Keystore (Android).
readString(key)
- Recupera o valor armazenado.
delete(key)
- Remove o valor armazenado.
Exemplo simplificado
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; class SecureStorageService { final FlutterSecureStorage _storage = const FlutterSecureStorage(); Future<void> saveString(String key, String value) async { await _storage.write(key: key, value: value); } Future<String?> readString(String key) async { return await _storage.read(key: key); } Future<void> delete(String key) async { await _storage.delete(key: key); } }
3.3 Integração Biometria + Storage
O fluxo esperado fica assim:
- Após login normal, salvar as credenciais no
SecureStorageService
. - Quando usuário optar pelo login biométrico:
BiometricService.authenticate()
→ se sucesso,SecureStorageService.readString()
→ recupera credenciais salvas,AuthService.signInWithPassword()
→ refaz login com Supabase.
3.4 Resumo
BiometricService
→ interação com hardware biométrico.SecureStorageService
→ armazenamento de credenciais de forma segura.- Juntas, permitem que a biometria seja usada como “chave” para liberar as credenciais e reexecutar o login.
4. Repository Layer
O repositório serve como fachada para os serviços de biometria e armazenamento seguro, expondo operações de alto nível para o domínio da aplicação.
Aqui não lidamos mais com detalhes do local_auth
ou do flutter_secure_storage
, mas sim com funções de negócio: salvar credenciais, checar disponibilidade e autenticar.
4.1 Interface IBiometricRepository
A interface define os métodos que a aplicação pode chamar, garantindo independência da implementação.
abstract class IBiometricRepository { Future<void> storeBiometricData(String data); Future<String?> authenticateWithBiometrics(String reason); Future<bool> hasBiometricData(); }
4.2 Implementação BiometricRepository
Aqui ocorre a integração real entre BiometricService
e SecureStorageService
.
import '/data/services/biometric/biometric_service.dart'; import '/data/services/secure_storage/secure_storage_service.dart'; class BiometricRepository implements IBiometricRepository { final BiometricService _biometricService; final SecureStorageService _storageService; static const _tokenKey = 'biometric_token'; BiometricRepository({ required BiometricService biometricService, required SecureStorageService storageService, }) : _biometricService = biometricService, _storageService = storageService; @override Future<void> storeBiometricData(String data) async { await _storageService.saveString(_tokenKey, data); } @override Future<String?> authenticateWithBiometrics(String reason) async { final available = await _biometricService.isAvailable(); if (!available) return null; final authenticated = await _biometricService.authenticate(reason: reason); if (!authenticated) return null; return await _storageService.readString(_tokenKey); } @override Future<bool> hasBiometricData() async { final stored = await _storageService.readString(_tokenKey); return stored != null; } }
4.3 Responsabilidades de cada método
storeBiometricData(String data)
- Executado após login normal.
- Salva a credencial (no caso atual, email/senha ou DTO serializado) de forma segura no dispositivo.
authenticateWithBiometrics()
- Primeiro valida se a biometria está disponível.
- Executa autenticação biométrica.
- Se sucesso, retorna a credencial previamente armazenada.
- Essa credencial é usada para refazer o login no Supabase.
hasBiometricData()
- Apenas verifica se existe algum dado salvo.
- Útil para decidir se o botão de login biométrico deve ser exibido na tela de login.
4.4 Ponto importante (limitação Supabase)
Devido à limitação atual do Supabase (que não fornece refresh tokens reutilizáveis), a credencial armazenada não é o refresh token, mas sim o par email/senha ou um DTO com os dados de login.
- Isso significa que o método
authenticateWithBiometrics()
retorna essas credenciais. - A camada de caso de uso (
AccountManagementUseCase
) então chama novamentesignInWithPassword()
para restaurar a sessão.
4.5 Resumo
O BiometricRepository
encapsula a biometria + armazenamento seguro e expõe três operações simples:
- Gravar credenciais.
- Validar biometria e recuperar credenciais.
- Verificar se existem dados para login biométrico.
5. Use Case / AuthService
A camada de Use Case / AuthService é onde a lógica de negócio é aplicada. Aqui conectamos o BiometricRepository com o UserAuthRepository (que fala com o Supabase).
5.1 Situação atual do Supabase (GoTrue v2, 2025)
- O Supabase utiliza o GoTrue v2 como provedor de autenticação.
- Diferente de versões anteriores, ele não gera refresh tokens reutilizáveis.
- O campo
session.refreshToken
retorna apenas um ID curto (ex.:lcixpkoex3e2
), que não funciona emauth.setSession()
. - Ao tentar usar esse valor em
setSession()
, o servidor retorna:
AuthApiException(message: Invalid Refresh Token: Refresh Token Not Found)
- Consequência: não é possível restaurar a sessão apenas com refresh token.
5.2 Implicação no login biométrico
- Em sistemas que suportam refresh tokens, o fluxo normal seria:
- Guardar refresh token após login.
- Validar biometria.
- Usar refresh token em
setSession()
→ restaurar sessão.
- Mas como isso não é suportado pelo Supabase, foi necessário adotar outra abordagem.
5.3 Solução implementada
Fluxo de login inicial (email/senha)
- Usuário faz login via
signInWithPassword()
. - Supabase retorna um
Session
comaccessToken
e umrefreshToken
inválido para reuso. - O app salva as credenciais do usuário (
email
esenha
) noSecureStorage
, criptografadas e protegidas por biometria.
Fluxo de login com biometria (segundo boot em diante)
- O usuário abre o app.
- A tela inicial verifica se existem credenciais armazenadas (
hasBiometricData()
). - Caso positivo → chama
BiometricService.authenticate()
. - Se sucesso → o app lê as credenciais salvas.
- Reexecuta
signInWithPassword()
com Supabase, recriando a sessão.
Isso significa que o “login biométrico” é, na prática, um atalho seguro para reaproveitar email/senha sem o usuário precisar digitar novamente.
5.4 Vantagens dessa abordagem
- Mantém a experiência do usuário fluida: basta usar biometria para logar.
- Não depende de recursos ainda indisponíveis no Supabase.
- Utiliza Secure Storage para proteger as credenciais.
- Permite fallback: se biometria falhar, o usuário pode sempre digitar email/senha.
5.5 Limitações
- Email e senha ficam armazenados localmente (mesmo que de forma criptografada).
- Se o Supabase no futuro habilitar refresh tokens reais, será necessário refatorar essa parte do fluxo para usar
setSession()
em vez designInWithPassword()
. - Em caso de alteração de senha pelo usuário, as credenciais salvas no dispositivo se tornam inválidas → exigindo login manual novamente.
5.6 Resumo
Na camada de Use Case/AuthService o papel da biometria é desbloquear credenciais salvas com segurança e reutilizá-las em um novo login no Supabase, contornando a limitação atual da plataforma.
6. UI / ViewModel
A camada de ViewModel expõe Commands que encapsulam chamadas assíncronas ao AccountManagementUseCase
.
A UI não conhece detalhes de serviços nem de repositórios: ela apenas executa os Command
s e reage aos seus estados (running
, result
, listeners).
6.1 ViewModel
No SignInViewModel
, os métodos relacionados à biometria são transformados em Command
s:
checkBiometricAvailability
→ executa_accountManagementUseCase.canUseBiometrics()
.- Indica se o dispositivo suporta biometria.
- Em caso de falha, retorna
CanNotUseBiometricsException
. checkBiometricData
→ executa_accountManagementUseCase.hasBiometricData()
.- Verifica se existem credenciais salvas no
SecureStorage
. - Caso não haja, retorna
CanNotUseBiometricsException
. biometricLogin
→ executa_accountManagementUseCase.loginWithBiometrics(String reason)
.- Solicita autenticação biométrica.
- Se sucesso → recupera credenciais armazenadas e refaz login com Supabase.
- Se falha → expõe a exceção à UI.
storeBiometricData
→ executa_accountManagementUseCase.storeBiometricData(String data)
.- Responsável por salvar credenciais após login manual com email/senha.
Dessa forma, o ViewModel
é apenas uma ponte de orquestração, sem lógica de autenticação.
6.2 UI
Na SignInView
, a biometria é integrada de forma condicional:
- Escuta dos Commands
addListener
emcheckBiometricAvailability
,checkBiometricData
ebiometricLogin
.- Atualiza variáveis locais (
_biometricAvailable
,_hasBiometricData
) conforme resultados. - Renderização do botão biométrico
- Apenas exibido se:
- Dispositivo suporta biometria.
- Há credenciais armazenadas.
- Caso contrário → ícone esmaecido ou oculto.
- Execução do login biométrico
- Ao toque no botão →
biometricLogin.execute("Autenticar com biometria...")
. - Enquanto
running == true
→ exibeCircularProgressIndicator
. - Ao concluir:
- Sucesso → redireciona para
Routes.events
. - Falha → apresenta
SnackBar
ouBottomSheetMessage
com motivo (cancelamento, ausência de sessão, erro genérico).
- Sucesso → redireciona para
6.3 Resumo
- ViewModel:
- Expõe apenas Commands, encapsulando casos de uso (
loginWithBiometrics
,storeBiometricData
, etc.). - Não contém lógica de UI, apenas traduz resultados em
Result<void>
. - UI:
- Decide quando exibir o botão de biometria.
- Executa os Commands e reage a seus estados (
running
,Success
,Failure
). - Fornece feedback claro ao usuário (loading, sucesso, falha).
Assim, a biometria no Sports segue fielmente o padrão Command + MVVM, mantendo separação entre infraestrutura, lógica de negócio e interface.
7. Conclusões
A biometria foi integrada ao Sports como um recurso de conveniência e segurança adicional, mas seu papel exato depende do contexto da aplicação.
- Fallback obrigatório
- O login com email/senha deve permanecer disponível.
- A biometria deve ser vista como atalho seguro, não como substituto único de autenticação.
- Logout e credenciais armazenadas
- No Sports, o logout encerra a sessão atual, mas pode manter credenciais no
SecureStorage
para permitir login biométrico subsequente. - Esse comportamento é aceitável em apps onde a conveniência pesa mais que a segurança absoluta.
- Em aplicações que lidam com dados sensíveis (ex.: bancos, saúde), o logout deve limpar totalmente o
SecureStorage
, impedindo qualquer reentrada sem autenticação completa.
- Limitação do Supabase
- O Supabase (GoTrue v2, 2025) ainda não fornece refresh tokens reutilizáveis.
- O campo
session.refreshToken
retorna apenas um identificador curto, que não funciona emauth.setSession()
. - Assim, a biometria hoje desbloqueia credenciais (email/senha) e refaz o login, em vez de restaurar sessão.
- Perspectiva futura
- Quando o Supabase liberar refresh tokens persistentes, será possível simplificar:
- Guardar apenas o refresh token.
- Restaurar a sessão via
auth.setSession(refreshToken)
.
- Isso permitirá maior segurança, sem precisar armazenar email/senha no dispositivo.
A biometria no Sports é hoje uma solução prática, que aumenta a conveniência do login e protege as credenciais no dispositivo.
Entretanto, a estratégia de logout e armazenamento deve sempre ser ajustada ao nível de sensibilidade dos dados da aplicação: para apps comuns, manter credenciais pode ser aceitável; para apps críticos, o correto é limpar tudo no logout.
Deixe uma resposta