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
MainActivitypara herdar deFlutterFragmentActivity:class MainActivity : FlutterFragmentActivity() - Sem isso,
local_authfalha com:PlatformException(no_fragment_activity, local_auth plugin requires activity to be a FragmentActivity.) - Linux
flutter_secure_storageprecisa estar registrado em:linux/flutter/generated_plugin_registrant.cclinux/flutter/generated_plugins.cmake
- Flutter SDK
- O
local_authexige SDK mínimo atualizado. No Sports, oflutterfoi 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.refreshTokenretorna 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
adbpara 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
MainActivityparaFlutterFragmentActivity. - 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
canCheckBiometricseisDeviceSupported. authenticate(reason)- Exibe o prompt nativo de autenticação biométrica.
- Retorna
true/falsede 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.refreshTokenretorna 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
SessioncomaccessTokene umrefreshTokeninválido para reuso. - O app salva as credenciais do usuário (
emailesenha) 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 Commands e reage aos seus estados (running, result, listeners).
6.1 ViewModel
No SignInViewModel, os métodos relacionados à biometria são transformados em Commands:
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
addListeneremcheckBiometricAvailability,checkBiometricDataebiometricLogin.- 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
SnackBarouBottomSheetMessagecom 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
SecureStoragepara 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.refreshTokenretorna 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