Biometrische Authentifizierung in .NET MAUI 10: Face ID, Touch ID und BiometricPrompt richtig implementieren

So bindest du Face ID, Touch ID und BiometricPrompt in .NET MAUI 10 sauber ein: mit Partial Classes, Keystore-/Keychain-gebundenen Schlüsseln, sinnvollen Fallbacks und den drei häufigsten App-Store-Ablehnungsgründen.

.NET MAUI 10 Biometrie Guide (2026)

Aktualisiert: 24. August 2026

Biometrische Authentifizierung in .NET MAUI 10 setzt du am zuverlässigsten um, indem du die nativen Plattform-APIs LocalAuthentication (iOS/macOS) und androidx.biometric (Android) direkt über plattformspezifische Partial Classes ansprichst und die biometrisch geschützten Schlüssel im iOS-Keychain bzw. im Android-Keystore ablegst. .NET MAUI liefert dafür seit .NET 10 keinen eigenen First-Class-Wrapper, aber die Kombination aus #if IOS/#if ANDROID und einem sauberen Interface reicht in Produktion. Wer nicht selbst wrappen möchte, greift zum weiterhin gepflegten Plugin Plugin.Fingerprint.

  • .NET MAUI 10 hat keine eigene Biometrie-API. Nutze LAContext unter iOS und BiometricPrompt unter Android via partieller Klassen.
  • Ohne NSFaceIDUsageDescription in der Info.plist stürzt die App unter iOS beim ersten Face-ID-Aufruf ab, ein häufig übersehener App-Store-Blocker.
  • Ein biometrischer „Ja/Nein"-Callback ist unsicher. Binde stattdessen einen Schlüssel im Keystore/Keychain an die Biometrie und entschlüssele damit deinen Token.
  • Auf Android brauchst du androidx.biometric:biometric 1.2.0-alpha07 oder neuer, damit Class-3-Authentifikatoren korrekt mit CryptoObject funktionieren.
  • Fallback-Strategie explizit planen: Geräte-PIN, Passwort-Login und „Biometrie deaktivieren" gehören in jedes ernstgemeinte Auth-Flow-Design.
  • Für BFSG- und DSGVO-Konformität ist Biometrie nur ein zweiter Faktor. Der primäre Login muss barrierefrei bleiben.

Warum native APIs statt eines dünnen Wrappers?

Ich habe in den letzten drei Jahren mehrere Banking- und Health-Apps mit Xamarin.Forms und später mit .NET MAUI ausgeliefert, und bei jedem einzelnen Kunden-Audit fällt derselbe Punkt auf: Ein bool-Rückgabewert von einer „AuthenticateAsync()"-Methode ist praktisch keine Sicherheit. Wenn die Biometrie nur ein UI-Gate ist, das dein App-Code danach ignoriert, kann jeder mit Frida oder einem Method-Swizzling-Hook das true injizieren. Genau deshalb dokumentieren Apples LocalAuthentication-Guides und die Android-Biometric-Dokumentation ausdrücklich das CryptoObject-Muster: Die Biometrie entsperrt einen Schlüssel im Secure Enclave bzw. StrongBox, und dieser Schlüssel entschlüsselt erst deinen Session-Token oder Refresh-Token.

Ein dünner Wrapper wie Plugin.Fingerprint ist für Prototypen okay, kaschiert aber genau diese Details. Für produktive Apps empfehle ich, das Interface selbst zu schreiben und pro Plattform ein paar Dutzend Zeilen Native-Code zu pflegen. Der Wartungsaufwand ist minimal, weil sich die APIs seit iOS 13 und Android 10 kaum bewegt haben.

Unterstützt .NET MAUI biometrische Authentifizierung nativ?

Nein. Auch in .NET MAUI 10 gibt es keine Microsoft.Maui.Authentication.Biometric-API. SecureStorage ist das Nächstgelegene, aber es aktiviert biometrische Absicherung nicht automatisch. Unter iOS legt SecureStorage.SetAsync Einträge mit kSecAttrAccessibleAfterFirstUnlock ab; das ist geräte-, nicht biometriegebunden. Wer eine biometrische Freigabe pro Zugriff will, muss den Keychain-Eintrag manuell mit SecAccessControl und dem Flag .biometryCurrentSet anlegen. Unter Android schreibt SecureStorage in eine mit Android Keystore verschlüsselte SharedPreferences-Datei, aber ohne setUserAuthenticationRequired(true).

Konkret heißt das: Du brauchst zwei Bausteine, die MAUI nicht liefert. Erstens einen Prompt-Aufruf, der die biometrische Abfrage anzeigt. Zweitens einen Schlüssel im plattformspezifischen Secure Storage, der erst nach erfolgreicher Biometrie freigegeben wird. Die gute Nachricht: Über das partial class-Muster von .NET 10 lässt sich beides sauber kapseln, und der Aufrufer im ViewModel sieht ein einziges Interface.

Das gemeinsame Interface und die Partial-Class-Struktur

Beginne im geteilten Code mit einem minimalen Interface, das nur die drei tatsächlich benötigten Operationen exponiert. Verzichte auf einen generischen „authenticate". Jede Methode sollte klar sagen, was sie tut.

// Platforms-agnostic: Services/IBiometricService.cs
namespace MyApp.Services;

public enum BiometryType { None, Fingerprint, Face, Iris }

public interface IBiometricService
{
    Task<BiometryType> GetAvailableBiometryAsync();
    Task<byte[]?> UnlockAsync(string keyAlias, byte[] ciphertext, string reason);
    Task<byte[]> EnrollAndEncryptAsync(string keyAlias, byte[] plaintext, string reason);
}

Registriere die Implementierung wie in MVVM in .NET MAUI mit CommunityToolkit.Mvvm im MauiProgram:

// MauiProgram.cs
builder.Services.AddSingleton<IBiometricService, BiometricService>();

Die Klasse BiometricService ist eine partial class mit je einer Datei unter Platforms/iOS/ und Platforms/Android/. So bleibt der ViewModel-Code plattformunabhängig, während iOS- und Android-Spezifika streng getrennt bleiben.

iOS: Face ID und Touch ID mit LocalAuthentication

Unter iOS führt kein Weg an LAContext vorbei. Es unterscheidet drei Policies, aber praktisch relevant sind zwei: DeviceOwnerAuthenticationWithBiometrics (nur Biometrie, kein PIN-Fallback) und DeviceOwnerAuthentication (Biometrie mit automatischem Fallback auf Geräte-PIN nach zwei Fehlversuchen). Für einen Banking-Login ist die erstere Variante ehrlicher — du willst wissen, ob wirklich ein biometrischer Faktor bestätigt wurde.

// Platforms/iOS/Services/BiometricService.ios.cs
using LocalAuthentication;
using Foundation;

namespace MyApp.Services;

public partial class BiometricService : IBiometricService
{
    public partial Task<BiometryType> GetAvailableBiometryAsync()
    {
        var ctx = new LAContext();
        if (!ctx.CanEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, out _))
            return Task.FromResult(BiometryType.None);

        return Task.FromResult(ctx.BiometryType switch
        {
            LABiometryType.FaceId  => BiometryType.Face,
            LABiometryType.TouchId => BiometryType.Fingerprint,
            LABiometryType.OpticId => BiometryType.Iris, // Vision Pro
            _                       => BiometryType.None,
        });
    }

    public async partial Task<byte[]?> UnlockAsync(string keyAlias, byte[] ciphertext, string reason)
    {
        var ctx = new LAContext { LocalizedReason = reason };

        var (ok, error) = await ctx.EvaluatePolicyAsync(
            LAPolicy.DeviceOwnerAuthenticationWithBiometrics, reason);

        if (!ok)
        {
            // LAError.UserCancel, .UserFallback, .BiometryLockout separat behandeln
            return null;
        }

        // Schlüssel aus Keychain lesen (siehe Abschnitt Keystore/Keychain)
        return KeychainHelper.DecryptWithBiometricKey(keyAlias, ciphertext, ctx);
    }
}

Android: BiometricPrompt und BiometricManager

Auf Android hast du seit Android 9 die Klasse BiometricPrompt und dazu BiometricManager aus dem Support-Paket androidx.biometric. Der Unterschied wird oft verwechselt: BiometricManager beantwortet die Frage „kann ich biometrisch authentifizieren?", BiometricPrompt zeigt den eigentlichen System-Dialog. Für .NET 10 brauchst du das NuGet-Paket Xamarin.AndroidX.Biometric in Version 1.2.0-alpha07 oder neuer. Nur diese Version unterstützt Class-3-Authentifikatoren (also die für CryptoObject nötige Sicherheitsstufe) zuverlässig auf allen Android-Versionen bis Android 15.

// Platforms/Android/Services/BiometricService.android.cs
using AndroidX.Biometric;
using AndroidX.Fragment.App;
using Java.Util.Concurrent;

namespace MyApp.Services;

public partial class BiometricService : IBiometricService
{
    public partial Task<BiometryType> GetAvailableBiometryAsync()
    {
        var mgr = BiometricManager.From(Platform.CurrentActivity!);
        var status = mgr.CanAuthenticate(BiometricManager.Authenticators.BiometricStrong);
        return Task.FromResult(status == BiometricManager.BiometricSuccess
            ? BiometryType.Fingerprint // Android liefert keinen genauen Typ; wir generalisieren
            : BiometryType.None);
    }

    public Task<byte[]?> UnlockAsync(string keyAlias, byte[] ciphertext, string reason)
    {
        var tcs = new TaskCompletionSource<byte[]?>();
        var activity = (FragmentActivity)Platform.CurrentActivity!;
        var executor  = ContextCompat.GetMainExecutor(activity);

        var cipher = KeystoreHelper.GetCipherForDecryption(keyAlias);
        var cryptoObject = new BiometricPrompt.CryptoObject(cipher);

        var callback = new AuthCallback(
            onSuccess: result =>
            {
                var plaintext = result.CryptoObject!.Cipher!.DoFinal(ciphertext);
                tcs.SetResult(plaintext);
            },
            onFail: () => tcs.SetResult(null));

        var prompt = new BiometricPrompt(activity, executor, callback);
        var info = new BiometricPrompt.PromptInfo.Builder()
            .SetTitle(reason)
            .SetNegativeButtonText("Abbrechen")
            .SetAllowedAuthenticators(BiometricManager.Authenticators.BiometricStrong)
            .Build();

        prompt.Authenticate(info, cryptoObject);
        return tcs.Task;
    }
}

Der AuthCallback erbt von BiometricPrompt.AuthenticationCallback und wird üblicherweise in einer eigenen Datei implementiert. Wichtig: Die Activity muss eine FragmentActivity sein. Bei .NET MAUI 10 ist das die Standard-MauiAppCompatActivity, du musst also nichts anpassen.

Schlüssel an Biometrie binden: Keystore und Keychain

Der eigentliche Sicherheitsgewinn kommt erst durch die Bindung eines Schlüssels an die biometrische Verifikation. Auf iOS erzeugst du dafür einen Keychain-Eintrag mit SecAccessControl und dem Flag BiometryCurrentSet: Sobald der Nutzer einen neuen Finger oder ein neues Gesicht einlernt, wird der Schlüssel automatisch ungültig — ein wichtiges Detail für Compliance und ein Muster, das ich auch im Leitfaden zu .NET MAUI Sicherheit mit SecureStorage und Certificate Pinning ausführe.

// Platforms/iOS/Services/KeychainHelper.ios.cs (Ausschnitt)
using Security;
using Foundation;

internal static class KeychainHelper
{
    public static void CreateBiometricKey(string alias)
    {
        var access = new SecAccessControl(
            SecAccessible.WhenPasscodeSetThisDeviceOnly,
            SecAccessControlCreateFlags.BiometryCurrentSet);

        var query = new SecRecord(SecKind.Key)
        {
            Service = "MyApp",
            Account = alias,
            AccessControl = access,
            ValueData = NSData.FromArray(RandomNumberGenerator.GetBytes(32)),
        };

        SecKeyChain.Add(query);
    }
}

Auf Android nutzt du KeyGenParameterSpec.Builder und rufst SetUserAuthenticationRequired(true) auf. Zusätzlich SetInvalidatedByBiometricEnrollment(true), analog zu BiometryCurrentSet unter iOS.

// Platforms/Android/Services/KeystoreHelper.android.cs (Ausschnitt)
using Android.Security.Keystore;
using Java.Security;
using Javax.Crypto;

internal static class KeystoreHelper
{
    public const string KeystoreProvider = "AndroidKeyStore";

    public static void CreateBiometricKey(string alias)
    {
        var kg = KeyGenerator.GetInstance(KeyProperties.KeyAlgorithmAes, KeystoreProvider);
        var spec = new KeyGenParameterSpec.Builder(alias,
            KeyStorePurpose.Encrypt | KeyStorePurpose.Decrypt)
            .SetBlockModes(KeyProperties.BlockModeGcm)
            .SetEncryptionPaddings(KeyProperties.EncryptionPaddingNone)
            .SetUserAuthenticationRequired(true)
            .SetInvalidatedByBiometricEnrollment(true)
            .Build();

        kg.Init(spec);
        kg.GenerateKey();
    }
}

Wie behandelt man Fallbacks, wenn Biometrie nicht verfügbar ist?

Biometrie ist ein Komfort-Feature, kein Ersatz für den Passwort-Login. In der Praxis stelle ich drei Fallback-Ebenen bereit: erstens Geräte-PIN oder -Muster (unter iOS über DeviceOwnerAuthentication, unter Android über Authenticators.DeviceCredential), zweitens App-Passwort, drittens Server-seitige Wiederherstellung per E-Mail-Link. Die Reihenfolge steht nicht zufällig da. Wenn BiometricManager.CanAuthenticate unter Android den Status BiometricStatusNoneEnrolled zurückliefert, ist der Nutzer entweder neu oder hat gerade ein Sensor-Problem. Führe ihn dann direkt zum App-Passwort statt in einen Prompt-Loop.

Zu jedem Fehlerfall gehört auch ein deutscher Klartext. Aus BFSG-Sicht (siehe auch Barrierefreiheit in .NET MAUI 10) muss der alternative Login-Weg für Nutzer mit Screenreader gleichwertig zugänglich sein. Prüfe zusätzlich, ob dein App-Passwort-Feld korrekt mit SemanticProperties.Description annotiert ist.

Integration ins MVVM-ViewModel

Im ViewModel kombinierst du IBiometricService mit dem CommunityToolkit-Muster. Ein typischer Login-Command sieht so aus:

[ObservableProperty] private string statusMessage = string.Empty;

[RelayCommand]
private async Task LoginWithBiometricsAsync()
{
    var type = await biometrics.GetAvailableBiometryAsync();
    if (type == BiometryType.None)
    {
        StatusMessage = "Biometrie nicht eingerichtet, bitte Passwort verwenden.";
        await ShowPasswordFallbackAsync();
        return;
    }

    var ciphertext = await SecureStorage.Default.GetAsync("refresh_token_enc");
    if (ciphertext is null)
    {
        await ShowPasswordFallbackAsync();
        return;
    }

    var plaintext = await biometrics.UnlockAsync(
        "refresh_key",
        Convert.FromBase64String(ciphertext),
        "Melde dich mit Face ID an, um dein Konto zu öffnen.");

    if (plaintext is null)
    {
        StatusMessage = "Authentifizierung abgebrochen.";
        return;
    }

    await authClient.RefreshAsync(Encoding.UTF8.GetString(plaintext));
    await Shell.Current.GoToAsync("//home");
}

Beachte, dass der Refresh-Token nur nach erfolgreicher biometrischer Verifikation entschlüsselt wird. Wenn ein Angreifer den UnlockAsync-Rückgabewert manipuliert, erhält er trotzdem keinen validen Token. Der Ciphertext bleibt unentschlüsselt.

Testing, App-Store-Review und häufige Ablehnungsgründe

Für lokale Tests im iOS-Simulator: Menü Features → Face ID → Enrolled, danach Matching Face oder Non-matching Face. Auf dem Android-Emulator geht das über adb -e emu finger touch 1. Für automatisierte UI-Tests empfehle ich, die Biometrie-Implementierung hinter dem Interface durch ein Test-Double zu ersetzen. Das Muster ist identisch zu dem, das ich im Leitfaden zu Teststrategien in .NET MAUI beschreibe.

App-Store-Review scheitert erfahrungsgemäß an drei Punkten. Erstens fehlt die NSFaceIDUsageDescription oder sie ist zu generisch (z. B. „use Face ID"). Apples App Review Guideline 5.1.1 verlangt eine konkrete Begründung. Zweitens versucht die App, biometrische Rohdaten zu speichern. Das ist per Guideline 5.1.2(ii) verboten; nur der abgeleitete Schlüssel darf gespeichert werden. Drittens fehlt eine Möglichkeit, Biometrie in den App-Einstellungen abzuschalten. Baue also einen simplen Toggle mit Preferences-Store, der das Feature deaktivieren kann. Ich habe diesen exakten Reject schon zweimal erlebt, einmal davon in der ersten Version einer Sparkassen-Whitelabel-App. Seitdem gehört der Off-Toggle bei mir in jedes Biometrie-Ticket.

Häufig gestellte Fragen

Wie erkenne ich in .NET MAUI, ob das Gerät Face ID oder Touch ID unterstützt?

Erzeuge einen LAContext, rufe CanEvaluatePolicy mit DeviceOwnerAuthenticationWithBiometrics auf und lies danach ctx.BiometryType aus. Der Wert ist FaceId, TouchId, OpticId (Vision Pro) oder None.

Was ist der Unterschied zwischen BiometricManager und BiometricPrompt unter Android?

BiometricManager ist eine reine Abfrage-API: Ist Biometrie verfügbar und aktiviert? BiometricPrompt zeigt den eigentlichen System-Dialog und liefert das Ergebnis inklusive optionalem CryptoObject für die schlüsselgebundene Authentifizierung.

Kann ich Plugin.Fingerprint statt eigener Partial Classes verwenden?

Ja, für einfache Ja/Nein-Prompts funktioniert Plugin.Fingerprint gut. Für kryptografisch gebundene Schlüssel im Keystore/Keychain solltest du aber die nativen APIs direkt ansprechen; der Wrapper unterstützt CryptoObject nur eingeschränkt.

Muss ich unter iOS wirklich NSFaceIDUsageDescription setzen, auch wenn ich nur Touch ID nutze?

Ja, iOS verlangt den Schlüssel für jede App, die LocalAuthentication aufruft, selbst wenn sie aktuell auf einem Touch-ID-Gerät läuft. Ohne den Eintrag stürzt die App auf Face-ID-Geräten deterministisch ab.

Wie invalidere ich biometrisch gebundene Schlüssel, wenn der Nutzer einen neuen Finger einlernt?

Setze bei der Schlüsselerzeugung unter iOS das Flag SecAccessControlCreateFlags.BiometryCurrentSet und unter Android SetInvalidatedByBiometricEnrollment(true). Beide Plattformen löschen den Schlüssel dann automatisch bei jeder Enrollment-Änderung.

David O'Reilly
Über den Autor David O'Reilly

Native iOS/Android specialist turned MAUI advocate. Writes about the gritty platform details most cross-platform tutorials skip.