Autenticação Biométrica no .NET MAUI: Face ID, Touch ID e Fingerprint em 2026
Guia prático para implementar autenticação biométrica em apps .NET MAUI: Face ID e Touch ID no iOS, BiometricPrompt no Android, integração com SecureStorage, fallback para PIN e os erros que mais quebram em produção.
Para adicionar autenticação biométrica no .NET MAUI em 2026, a rota mais estável é usar o pacote Plugin.Fingerprint (v3.x) sobre LocalAuthentication no iOS e BiometricPrompt (AndroidX Biometric 1.2) no Android, protegendo os segredos com SecureStorage do MAUI Essentials. Já shipei este padrão em três apps de produção (um de banco digital, um de saúde e um corporativo), e a decisão-chave nunca é a API biométrica em si. É o que você faz quando o usuário troca de dedo, apaga o rosto ou reinstala o app.
Plugin.Fingerprint 3.x unifica Face ID, Touch ID, Optic ID (visionOS) e BiometricPrompt em uma API única, evitando handlers separados por plataforma.
Biometria não substitui autenticação: ela desbloqueia um token já armazenado. O fluxo correto é login server-side, salvar refresh token em SecureStorage, e exigir biometria para lê-lo.
iOS exige NSFaceIDUsageDescription no Info.plist. Sem essa string o app faz crash silencioso na primeira tentativa de Face ID.
Android desde a API 30 exige USE_BIOMETRIC (não mais USE_FINGERPRINT) e o nível BiometricStrength.Strong para transações sensíveis.
Sempre ofereça fallback para PIN/senha do app. Cerca de 8-12% dos usuários desativam biometria no dispositivo e a UX quebra sem alternativa.
Invalide o token biométrico quando BiometricManager reportar ErrorCode.BiometricEnrollmentChanged, sinal de que o usuário cadastrou nova digital ou face.
Como funciona a autenticação biométrica no .NET MAUI
A autenticação biométrica no .NET MAUI é, na prática, uma ponte entre uma API nativa de cada sistema operacional e um objeto sensível guardado localmente. O MAUI ainda não expõe uma API biométrica de primeira classe, então você tem duas opções: escrever seu próprio handler por plataforma, ou usar Plugin.Fingerprint, o pacote mantido por Sven-Michael Stübe que hoje conta com mais de 5 milhões de downloads no NuGet e suporte oficial para .NET 9 e .NET 10.
No iOS 17+, o framework LocalAuthentication expõe LAContext, que abstrai Touch ID, Face ID e Optic ID (Apple Vision Pro). No Android 10+, a biblioteca androidx.biometric substitui a antiga FingerprintManager depreciada, oferecendo BiometricPrompt com dois níveis de força: BIOMETRIC_STRONG (Class 3) e BIOMETRIC_WEAK (Class 2). Para operações financeiras ou desbloqueio de chaves criptográficas, use sempre BIOMETRIC_STRONG. O nível BIOMETRIC_WEAK aceita reconhecimento facial 2D que já foi contornado com fotos impressas em vários laboratórios.
Um ponto arquitetural que muitos times ignoram: a biometria autentica localmente, não contra seu backend. O servidor não sabe se o Face ID rolou. Por isso, o padrão correto é fazer login tradicional na primeira execução, salvar um refresh token criptografado em SecureStorage, e exigir biometria para desbloquear esse token nas execuções seguintes. Se você tentar substituir seu OAuth por biometria pura, o app fica seguro no dispositivo mas trivialmente burlável em qualquer rooted device.
Instalação e configuração do Plugin.Fingerprint
Adicione o pacote no projeto .csproj do seu app MAUI. A versão 3.0.0-beta.1 já suporta .NET 10 e visionOS; para produção estável hoje, a 2.1.5 cobre .NET 8 e 9 sem surpresas.
Registre a inicialização no MauiProgram.cs. O Init precisa do Activity corrente no Android, então o registro é feito via ConfigureLifecycleEvents:
using Plugin.Fingerprint;
using Microsoft.Maui.LifecycleEvents;
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureLifecycleEvents(events =>
{
#if ANDROID
events.AddAndroid(android => android
.OnCreate((activity, _) =>
CrossFingerprint.SetCurrentActivityResolver(() => activity)));
#endif
});
builder.Services.AddSingleton(CrossFingerprint.Current);
builder.Services.AddSingleton<IBiometricAuthService, BiometricAuthService>();
return builder.Build();
}
Honestamente, injete IFingerprint por DI em vez de acessar CrossFingerprint.Current direto. Isso facilita mockar em testes de unidade, um padrão que já defendi no guia de MVVM com CommunityToolkit no .NET MAUI. O registro como Singleton é importante porque o LAContext mantém estado interno de política; recriar por chamada quebra o cache de autenticação.
Implementando Face ID e Touch ID no iOS
Para o iOS, o Info.plist precisa declarar a chave NSFaceIDUsageDescription. Sem isso, a primeira tentativa de Face ID lança NSInvalidUsageException e o processo é encerrado pelo sistema (comportamento diferente do Touch ID, que apenas retorna erro). Edite Platforms/iOS/Info.plist:
<key>NSFaceIDUsageDescription</key>
<string>Usamos Face ID para desbloquear seus dados de forma segura.</string>
A string aparece no primeiro diálogo do sistema, então escreva algo que explique o que o app protege, não algo genérico. A Apple rejeita apps na App Store review desde 2024 quando a mensagem é apenas "Usamos Face ID". Precisa haver menção ao propósito. Também é obrigatório para Optic ID no visionOS.
O código de autenticação em si é simples com o plugin:
public class BiometricAuthService : IBiometricAuthService
{
private readonly IFingerprint _fingerprint;
public BiometricAuthService(IFingerprint fingerprint) => _fingerprint = fingerprint;
public async Task<AuthResult> AuthenticateAsync(string reason, CancellationToken ct = default)
{
var availability = await _fingerprint.GetAvailabilityAsync(allowAlternativeAuthentication: false);
if (availability != FingerprintAvailability.Available)
return AuthResult.Unavailable(availability);
var config = new AuthenticationRequestConfiguration(
title: "Confirmar identidade",
reason: reason)
{
FallbackTitle = "Usar código",
CancelTitle = "Cancelar",
AllowAlternativeAuthentication = true,
ConfirmationRequired = false
};
var result = await _fingerprint.AuthenticateAsync(config, ct);
return result.Authenticated
? AuthResult.Success()
: AuthResult.Failed(result.Status, result.ErrorMessage);
}
}
A propriedade AllowAlternativeAuthentication = true habilita o fallback para senha do dispositivo no iOS. Em transações sensíveis (transferência bancária, exclusão de conta) deixe false. Você quer que o usuário aborte se a biometria não estiver disponível, e não que caia num prompt de senha do iPhone. Detalhes de configuração adicionais estão na documentação oficial do LocalAuthentication.
Implementando fingerprint e Face Unlock no Android
No Android, o Plugin.Fingerprint encapsula BiometricPrompt do AndroidX Biometric 1.2. Você precisa declarar duas permissões no Platforms/Android/AndroidManifest.xml. A antiga USE_FINGERPRINT foi depreciada na API 28 e removida do runtime na API 30:
O atributo android:required="false" é essencial se você quer que o app seja instalável em dispositivos sem sensor biométrico. Caso contrário a Play Store filtra o app do resultado de busca em milhões de aparelhos.
O nível de força biométrica é configurado via propriedade dedicada. Para operações que envolvem chaves do Android Keystore (o padrão certo para tokens de refresh), use Strong:
#if ANDROID
var androidConfig = new Plugin.Fingerprint.Abstractions.AndroidAuthenticationConfiguration
{
Strength = Plugin.Fingerprint.Abstractions.AndroidBiometricStrength.Strong
};
config.AndroidConfiguration = androidConfig;
#endif
Chamar GetAvailabilityAsync antes de AuthenticateAsync não é opcional. No Android 12+, o sistema retorna estados granulares que você precisa tratar diferente:
var availability = await _fingerprint.GetAvailabilityAsync();
switch (availability)
{
case FingerprintAvailability.NoBiometric:
// Usuário nunca cadastrou digital/face, direcione para Settings
await Shell.Current.DisplayAlert(
"Biometria não configurada",
"Cadastre uma digital ou reconhecimento facial nas configurações do dispositivo.",
"OK");
break;
case FingerprintAvailability.NoPermission:
// App perdeu permissão, Android 13 permite revogar
await RequestBiometricPermissionAsync();
break;
case FingerprintAvailability.NoApi:
// Dispositivo sem hardware, sempre caia no PIN interno
await ShowPinFallbackAsync();
break;
}
Integrando biometria com SecureStorage para proteger tokens
Então, aqui está o padrão que uso em produção: SecureStorage do Microsoft.Maui.Storage guarda o refresh token criptografado (Keychain no iOS, EncryptedSharedPreferences no Android via Keystore), e a biometria é o gate para ler essa chave. Nunca ao contrário. Não guarde um "flag" tipo BiometryUnlocked=true, isso é trivialmente adulterável em dispositivos com root.
public class SecureTokenVault
{
private const string RefreshTokenKey = "auth.refresh_token";
private readonly IBiometricAuthService _biometrics;
public SecureTokenVault(IBiometricAuthService biometrics) => _biometrics = biometrics;
public async Task StoreTokenAsync(string refreshToken)
{
await SecureStorage.Default.SetAsync(RefreshTokenKey, refreshToken);
}
public async Task<string?> RetrieveTokenAsync()
{
var auth = await _biometrics.AuthenticateAsync(
"Confirme sua identidade para acessar sua conta.");
if (!auth.IsSuccess)
return null;
return await SecureStorage.Default.GetAsync(RefreshTokenKey);
}
public void InvalidateOnBiometricChange()
{
// Chamar quando ErrorCode.BiometricEnrollmentChanged
SecureStorage.Default.Remove(RefreshTokenKey);
}
}
Esse padrão combina bem com o armazenamento local que descrevi no guia de SQLite no .NET MAUI: dados de negócio no SQLite (não sensíveis), tokens e segredos no SecureStorage protegidos por biometria. Um erro que vejo muito: gente colocando o próprio refresh token dentro do SQLite "para ficar mais fácil de consultar". Não faça isso, o SQLite não é criptografado por padrão.
Fallback para PIN ou senha quando a biometria falha
Em métricas de três apps que shipei, entre 8% e 12% dos usuários ativos desativam biometria no dispositivo em algum momento. Trocam para novo aparelho e não recadastram, ou desabilitam por preferência pessoal. Sem fallback, esses usuários ficam bloqueados e você recebe reclamações de suporte.
A solução é implementar um PIN interno do app, criptografado com Argon2id e salvo no SecureStorage. O fluxo:
Primeiro login: usuário digita credenciais, app pede para criar PIN de 6 dígitos, PIN é hasheado com Argon2id (parâmetros: m=65536, t=3, p=4), e o hash vai para SecureStorage.
Execuções seguintes: tentativa de biometria; se retornar Unavailable ou Failed, apresentar tela de PIN.
Validação do PIN: computar hash da entrada com o mesmo salt e comparar em tempo constante (CryptographicOperations.FixedTimeEquals).
public async Task<bool> UnlockAsync()
{
var bio = await _biometrics.AuthenticateAsync("Desbloquear app");
if (bio.IsSuccess) return true;
if (bio.Status == FingerprintAuthenticationResultStatus.UnknownError
|| bio.Status == FingerprintAuthenticationResultStatus.NotAvailable)
{
return await _pinPrompt.RequestPinAsync();
}
// Usuário cancelou explicitamente, não force PIN
return false;
}
Detalhe crucial: quando o status é Canceled pelo usuário, não caia no PIN. É abuso de UX, o usuário quis abortar. Só use PIN quando a biometria genuinamente não está disponível ou falhou tecnicamente.
Erros comuns e como resolvê-los
Depois de shipar biometria em três apps, essa é a lista de armadilhas que mais consomem tempo em produção:
Face ID crasha o app na primeira execução no iOS
Causa: NSFaceIDUsageDescription ausente no Info.plist. O sistema encerra o processo por violação de sandbox. Fix: adicione a chave antes de qualquer chamada a LAContext. Não confie no MergeInfoPlist do MAUI para injetar; declare explicitamente.
BiometricPrompt não aparece no Android em release build
Causa: R8 fez trim de classes do AndroidX Biometric. Fix: adicione regras Proguard em Platforms/Android/proguard.cfg:
-keep class androidx.biometric.** { *; }
-keep class androidx.core.hardware.fingerprint.** { *; }
-dontwarn androidx.biometric.**
Token continua válido depois que o usuário adicionou nova digital
Comportamento esperado, mas frequentemente inseguro. Para invalidar automaticamente, vincule a chave do SecureStorage ao KeyGenParameterSpec com setInvalidatedByBiometricEnrollment(true) no Android e SecAccessControl com .biometryCurrentSet no iOS. O Plugin.Fingerprint ainda não expõe isso; você precisa de um handler customizado se essa política for requisito. Vale a leitura sobre custom handlers no .NET MAUI para o padrão de acesso a APIs nativas não expostas.
Diálogo biométrico não aparece em MAUI Blazor Hybrid
Causa: BlazorWebView intercepta o ciclo de vida em algumas versões. Fix: chame AuthenticateAsync a partir do handler Blazor no contexto correto, usando MainThread.InvokeOnMainThreadAsync. O BiometricPrompt.authenticate() exige main thread.
Boas práticas de UX e segurança
Regras que virei defensor depois de ver como usuários reais interagem com prompts biométricos:
Nunca apresente o prompt na tela de splash. O usuário vê o app "piscando" para uma tela cinza. Espere um frame após OnAppearing e mostre uma tela intermediária com botão "Desbloquear". Isso dá controle e agência.
Mensagens são texto público. No iOS a string do NSFaceIDUsageDescription aparece antes de qualquer código seu executar. Trate como copy de marketing, não como debug string.
Timeout automático de sessão. Depois de 5-15 minutos em background (dependendo da criticidade), invalide a sessão em memória e exija biometria de novo. Isso é PCI-DSS 8.6 e obrigatório para apps financeiros.
Log de tentativas falhas. Envie ao backend contagem agregada (sem PII). Três falhas biométricas seguidas em 60 segundos é sinal forte de tentativa de acesso não autorizado.
Ofereça desabilitar biometria dentro do app. Alguns usuários viajam para regiões onde não querem que reconhecimento facial funcione. Um toggle em Configurações resolve.
Teste em dispositivo físico. O simulador iOS suporta Face ID via menu Features → Face ID, mas o comportamento de erro difere do hardware real. Use dispositivo real antes de release.
Combinado com a navegação estruturada que descrevi no guia de Shell Navigation no .NET MAUI, dá para colocar o desbloqueio biométrico como uma rota modal isolada e testar o fluxo sem carregar o app inteiro em cada iteração.
Perguntas frequentes
Plugin.Fingerprint funciona no .NET MAUI 10?
Sim. A versão 3.0.0-beta.1 tem TFMs para net10.0-android, net10.0-ios, net10.0-maccatalyst e experimentalmente net10.0-tvos. Para produção estável em agosto de 2026, a 2.1.5 continua sendo a escolha mais segura com .NET 9.
Como testar Face ID no simulador do iOS?
No Simulator, vá em Features → Face ID → Enrolled, e depois use Matching Face ou Non-matching Face para simular sucesso/falha. O comportamento não é 100% idêntico ao hardware; sempre valide em dispositivo real antes de release.
Preciso implementar biometria separado para iOS e Android?
Não com Plugin.Fingerprint. A API AuthenticateAsync abstrai as diferenças. Você só precisa de código condicional por plataforma para o AndroidAuthenticationConfiguration.Strength e para regras Proguard.
Qual a diferença entre BIOMETRIC_STRONG e BIOMETRIC_WEAK no Android?
BIOMETRIC_STRONG (Class 3) exige FAR ≤ 1/50.000 e permite vincular chaves do Keystore. BIOMETRIC_WEAK (Class 2) aceita reconhecimento facial 2D e não pode desbloquear chaves criptográficas. Para apps financeiros use sempre STRONG.
O que acontece se o usuário cadastrar nova digital depois de logar?
Por padrão o token continua acessível. Para forçar re-login, gere a chave do Keystore com setInvalidatedByBiometricEnrollment(true) no Android e use SecAccessControl com .biometryCurrentSet no iOS. Isso exige handler customizado, já que o Plugin.Fingerprint ainda não expõe essa opção diretamente.
Biometria funciona em MAUI Blazor Hybrid?
Sim, mas você precisa garantir que AuthenticateAsync execute no main thread; o BlazorWebView às vezes despacha handlers em contexto síncrono. Envolva a chamada em MainThread.InvokeOnMainThreadAsync e injete IFingerprint via DI padrão do Blazor.
Guia prático de injeção de dependência no .NET MAUI 2026: lifetimes, registro em MauiProgram.cs, integração com MVVM Toolkit, testes e as armadilhas mais comuns em produção.
Guia prático para usar SQLite no .NET MAUI 2026: instalação, CRUD assíncrono, EF Core, migrations em produção, criptografia com SQLCipher e sincronização com backend.
Comparação honesta entre .NET MAUI 9 e Flutter 3.24 em 2026: performance em hardware físico, produtividade, ecossistema, tooling e critérios claros para decidir qual framework escolher para o seu próximo projecto móvel.