المصادقة البيومترية في .NET MAUI: دليل شامل لـ Face ID و Touch ID و BiometricPrompt في 2026

دليل عملي كامل للمصادقة البيومترية في .NET MAUI 10: تنفيذ Face ID و Touch ID و BiometricPrompt مع SecureStorage، أخطاء الإنتاج الفعلية وطرق معالجتها.

المصادقة البيومترية في MAUI 2026

آخر تحديث: 10 أغسطس 2026

المصادقة البيومترية في .NET MAUI هي عملية التحقق من هوية المستخدم عبر بصمة الإصبع أو التعرف على الوجه باستخدام واجهات النظام الأصلية: LocalAuthentication على iOS وBiometricPrompt على Android. في مشاريع MAUI الحديثة (إصدار 10 وأعلى)، الطريق العملي هو استخدام Plugin.Maui.Biometric أو كتابة معالج أصلي (Handler) رفيع فوق هذه الواجهات، مع تخزين الأسرار في SecureStorage المدعوم بـ Keychain أو Android Keystore. صراحةً، شحنت هذا النمط في ثلاثة تطبيقات مالية خلال العام الماضي، وسأعطيك في هذا الدليل كل الأخطاء التي دفعت ثمنها فعلياً قبل أن تصل إلى إنتاجك.

  • iOS يستخدم LAContext من إطار عمل LocalAuthentication مع سياستين: DeviceOwnerAuthenticationWithBiometrics وDeviceOwnerAuthentication (تدعم الرمز السري كاحتياطي).
  • Android منذ API 28 يستخدم BiometricPrompt، والطريقة الموصى بها في 2026 هي مكتبة androidx.biometric التي تعمل حتى API 23.
  • يجب إضافة NSFaceIDUsageDescription في Info.plist وإلا سيرفض App Store التطبيق ويتعطل عند أول محاولة استخدام Face ID.
  • لا تخزن كلمات المرور بعد نجاح البصمة في Preferences. استخدم دائماً SecureStorage الذي يستدعي Keychain على iOS وEncryptedSharedPreferences على Android.
  • خطأ BiometricNotEnrolled يختلف جذرياً عن BiometricNotAvailable، وتجاهل هذا الفرق يعني تجربة مستخدم مكسورة على أجهزة كثيرة.
  • على Android، يجب طلب صلاحية USE_BIOMETRIC في AndroidManifest.xml (هي صلاحية عادية تُمنح تلقائياً، لكن غيابها يعني رفض BiometricPrompt).

لماذا المصادقة البيومترية في تطبيقات MAUI؟

في أي تطبيق يتعامل مع حسابات مصرفية، ملفات صحية، أو حتى مجرد جلسة عمل طويلة الأمد، طلب كلمة المرور في كل مرة يفتح فيها المستخدم التطبيق تجربة سيئة. المصادقة البيومترية تحل هذه المعضلة: يبقى الرمز الفعلي (access token أو refresh token) مخزناً بشكل آمن، ولا يُفكّ إلا بعد أن يثبت المستخدم حضوره الفيزيائي عبر بصمة أو وجه.

الفكرة المهمة التي يفوتها كثير من المطورين: البصمة ليست بديلاً عن المصادقة، بل هي بوابة محلية تحمي أسراراً مخزنة مسبقاً. من الناحية العملية، أنت تسجّل الدخول مرة واحدة عبر كلمة مرور أو OAuth، تخزن الرمز في SecureStorage، ثم في كل جلسة لاحقة تفتح البصمة القفل عن هذا الرمز. أبل توضح هذا المبدأ في وثائق LocalAuthentication الرسمية، وجوجل تكرر نفس النمط في دليل BiometricPrompt.

الميزة الثانية أن تشغيل البصمة يرفع من التزامك بمعايير مثل SOC 2 وHIPAA بشكل ملموس. مراجعو App Store يتوقعون هذا السلوك في أي تطبيق فئة "المالية" أو "الصحة" اعتباراً من مراجعات 2025-2026، والتطبيقات التي تعتمد على كلمة مرور فقط تُقابَل غالباً بطلب توضيح إضافي. إن كنت قادماً من عالم Xamarin.Forms، يمكنك مراجعة دليل الترقية إلى .NET MAUI 10 لفهم كيف تغيّرت أنظمة المعالجات (Handlers) بين النسختين قبل تنفيذ الطبقة الأصلية.

الواجهات الأصلية: LocalAuthentication و BiometricPrompt

قبل أي كود، لا بد من فهم الاختلاف الجوهري بين المنصتين، لأن أي مكتبة تدّعي إخفاء هذا الاختلاف تنتهي إلى قرارات تصميم مفروضة عليك.

الميزةiOS (LocalAuthentication)Android (BiometricPrompt)
الفئة الرئيسيةLAContextBiometricPrompt عبر androidx.biometric
أنواع البصمة المدعومةFace ID، Touch ID، Optic ID (Vision Pro)بصمة، وجه، قزحية (يعتمد على الجهاز)
الإذن المطلوبNSFaceIDUsageDescription فقط لـ Face IDUSE_BIOMETRIC في Manifest
الاحتياطي (Fallback)الرمز السري للجهاز عبر DeviceOwnerAuthenticationPIN/Pattern عبر setAllowedAuthenticators
الحد الأدنى للنظامiOS 8+ (Face ID منذ iOS 11)API 23 مع androidx، أصلياً API 28
التحقق من التوفرCanEvaluatePolicyBiometricManager.CanAuthenticate

الاختلاف الأهم عملياً: على iOS، البصمة والرمز السري هما جزء من نفس السياسة (DeviceOwnerAuthentication)، لكن على Android يجب أن تحدد الأنواع المسموح بها صراحة عبر Authenticators.BiometricStrong أو Authenticators.DeviceCredential. هذا يعني أن كودك سيحتاج مسارات مختلفة إذا أردت السماح بالرمز السري كبديل.

تجهيز المشروع: أذونات iOS و Android

ابدأ بمشروع MAUI جديد أو موجود يستهدف net10.0-android وnet10.0-ios. تحقق من إصدار العمل بأمر:

dotnet workload install maui
dotnet workload update
dotnet new maui -n BiometricDemo -f net10.0

iOS: تعديل Info.plist

افتح Platforms/iOS/Info.plist وأضف داخل <dict>:

<key>NSFaceIDUsageDescription</key>
<string>نستخدم Face ID لحماية جلستك وتسجيل دخولك بسرعة دون كتابة كلمة المرور في كل مرة.</string>

Android: تعديل AndroidManifest.xml

افتح Platforms/Android/AndroidManifest.xml وأضف داخل عنصر manifest قبل application:

<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<!-- للتوافق مع API قديمة قبل 28 -->
<uses-permission android:name="android.permission.USE_FINGERPRINT" />

تثبيت الحزم الأصلية

في ملف .csproj أضف حزمة androidx.biometric يدوياً لأن MAUI لا يجلبها افتراضياً:

<ItemGroup Condition="$(TargetFramework.Contains('-android'))">
  <PackageReference Include="Xamarin.AndroidX.Biometric" Version="1.2.0.5" />
  <PackageReference Include="Xamarin.AndroidX.Fragment.Ktx" Version="1.8.5" />
</ItemGroup>

على iOS لا تحتاج حزمة إضافية. إطار LocalAuthentication جزء من SDK الأساسي منذ سنوات ويكفي استيراده باستخدام using LocalAuthentication;.

تنفيذ واجهة موحدة عبر IBiometricService

الفكرة أن نحصر التفاصيل المنصية داخل تنفيذ Partial Class واحد، ونكشف واجهة نظيفة إلى الطبقة العليا. هذا يبقي ViewModels قابلة للاختبار بلا وهم. لمن يهمه الجانب الاختباري، راجع دليل اختبار تطبيقات .NET MAUI لفهم كيف تُحقن هذه الواجهة في اختبارات الوحدة.

1) تعريف الواجهة والنموذج

// Services/IBiometricService.cs
namespace BiometricDemo.Services;

public enum BiometricResult
{
    Success,
    Failed,
    Canceled,
    NotAvailable,
    NotEnrolled,
    LockedOut
}

public interface IBiometricService
{
    Task<bool> IsAvailableAsync();
    Task<BiometricResult> AuthenticateAsync(string reason, string cancelTitle = "إلغاء");
}

2) الملف الجزئي (Partial) المشترك

// Services/BiometricService.cs
namespace BiometricDemo.Services;

public partial class BiometricService : IBiometricService
{
    public partial Task<bool> IsAvailableAsync();
    public partial Task<BiometricResult> AuthenticateAsync(string reason, string cancelTitle);
}

3) التسجيل في MauiProgram.cs

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

لماذا Singleton وليس Transient؟ لأن الحالة الوحيدة التي نحتفظ بها هي مرجع لـ LAContext يمكن إعادة استخدامه، أو BiometricPrompt المرتبط بـ Activity الحالي. راجع دليل حقن التبعيات في .NET MAUI 10 للأنماط المفضلة في تسجيل الخدمات المرتبطة بحياة النافذة.

التعامل مع Face ID و Touch ID على iOS

هنا يأتي الجزء الذي لا تكشفه الطبقات المجردة: التمييز بين نوعي البصمة لعرض النص الصحيح في الواجهة. مستخدم Face ID يتوقع رؤية أيقونة الوجه، ومستخدم Touch ID يتوقع رؤية أيقونة البصمة. اقرأ BiometryType من LAContext بعد استدعاء CanEvaluatePolicy:

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

namespace BiometricDemo.Services;

public partial class BiometricService
{
    public partial Task<bool> IsAvailableAsync()
    {
        var context = new LAContext();
        var available = context.CanEvaluatePolicy(
            LAPolicy.DeviceOwnerAuthenticationWithBiometrics,
            out NSError _);
        return Task.FromResult(available);
    }

    public partial async Task<BiometricResult> AuthenticateAsync(
        string reason, string cancelTitle)
    {
        var context = new LAContext { LocalizedCancelTitle = cancelTitle };

        if (!context.CanEvaluatePolicy(
                LAPolicy.DeviceOwnerAuthenticationWithBiometrics,
                out NSError availabilityError))
        {
            return MapIosError(availabilityError);
        }

        var (ok, evalError) = await context.EvaluatePolicyAsync(
            LAPolicy.DeviceOwnerAuthenticationWithBiometrics, reason);

        if (ok) return BiometricResult.Success;
        return MapIosError(evalError);
    }

    private static BiometricResult MapIosError(NSError? error) => error?.Code switch
    {
        (long)LAStatus.UserCancel or (long)LAStatus.AppCancel
            or (long)LAStatus.SystemCancel => BiometricResult.Canceled,
        (long)LAStatus.BiometryNotAvailable => BiometricResult.NotAvailable,
        (long)LAStatus.BiometryNotEnrolled => BiometricResult.NotEnrolled,
        (long)LAStatus.BiometryLockout => BiometricResult.LockedOut,
        _ => BiometricResult.Failed
    };
}

لعرض النص المخصص بحسب نوع البصمة:

string GetBiometryTypeLabel()
{
    var ctx = new LAContext();
    ctx.CanEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, out _);
    return ctx.BiometryType switch
    {
        LABiometryType.FaceId => "استخدم Face ID",
        LABiometryType.TouchId => "استخدم بصمة الإصبع",
        _ => "استخدم المصادقة البيومترية"
    };
}

استخدام BiometricPrompt على Android

Google قدّمت BiometricPrompt في Android 9 (Pie) لتوحيد واجهة البصمة، ثم أطلقت مكتبة androidx.biometric التي تدعم API 23 فأعلى وتغلّف الاختلافات. في MAUI هذا هو المسار الوحيد المقبول للنشر في 2026 لأن FingerprintManager القديمة موسومة deprecated منذ API 28.

// Services/BiometricService.android.cs
using AndroidX.Biometric;
using AndroidX.Core.Content;
using AndroidX.Fragment.App;
using Java.Util.Concurrent;
using Microsoft.Maui.ApplicationModel;

namespace BiometricDemo.Services;

public partial class BiometricService
{
    public partial Task<bool> IsAvailableAsync()
    {
        var context = Platform.CurrentActivity ?? Android.App.Application.Context;
        var manager = BiometricManager.From(context);
        var status = manager.CanAuthenticate(
            (int)BiometricManager.Authenticators.BiometricStrong);
        return Task.FromResult(status == BiometricManager.BiometricSuccess);
    }

    public partial Task<BiometricResult> AuthenticateAsync(
        string reason, string cancelTitle)
    {
        var tcs = new TaskCompletionSource<BiometricResult>();
        var activity = Platform.CurrentActivity as FragmentActivity
            ?? throw new InvalidOperationException(
                "MainActivity يجب أن يرث من FragmentActivity");

        var executor = ContextCompat.GetMainExecutor(activity);
        var callback = new AuthCallback(tcs);
        var prompt = new BiometricPrompt(activity, executor, callback);

        var info = new BiometricPrompt.PromptInfo.Builder()
            .SetTitle("تأكيد الهوية")
            .SetSubtitle(reason)
            .SetNegativeButtonText(cancelTitle)
            .SetAllowedAuthenticators(
                (int)BiometricManager.Authenticators.BiometricStrong)
            .Build();

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

    private class AuthCallback : BiometricPrompt.AuthenticationCallback
    {
        private readonly TaskCompletionSource<BiometricResult> _tcs;
        public AuthCallback(TaskCompletionSource<BiometricResult> tcs) => _tcs = tcs;

        public override void OnAuthenticationSucceeded(
            BiometricPrompt.AuthenticationResult result)
            => _tcs.TrySetResult(BiometricResult.Success);

        public override void OnAuthenticationFailed()
            => _tcs.TrySetResult(BiometricResult.Failed);

        public override void OnAuthenticationError(int errorCode, Java.Lang.ICharSequence errString)
        {
            var mapped = errorCode switch
            {
                BiometricPrompt.ErrorUserCanceled
                    or BiometricPrompt.ErrorNegativeButton => BiometricResult.Canceled,
                BiometricPrompt.ErrorLockout
                    or BiometricPrompt.ErrorLockoutPermanent => BiometricResult.LockedOut,
                BiometricPrompt.ErrorNoBiometrics => BiometricResult.NotEnrolled,
                BiometricPrompt.ErrorHwNotPresent
                    or BiometricPrompt.ErrorHwUnavailable => BiometricResult.NotAvailable,
                _ => BiometricResult.Failed
            };
            _tcs.TrySetResult(mapped);
        }
    }
}

دمج SecureStorage مع البصمة لحماية الأسرار

القفل البيومتري بلا سر محمي وراءه هو مجرد شاشة عرض جميلة. الخطوة العملية هي تخزين access token في SecureStorage الذي يستدعي Keychain على iOS وEncryptedSharedPreferences على Android:

public class SessionManager
{
    private const string TokenKey = "auth_access_token";
    private readonly IBiometricService _bio;

    public SessionManager(IBiometricService bio) => _bio = bio;

    public async Task<string?> UnlockSessionAsync()
    {
        var result = await _bio.AuthenticateAsync(
            "افتح جلستك عبر البصمة للوصول إلى بياناتك");

        if (result != BiometricResult.Success)
            return null;

        return await SecureStorage.Default.GetAsync(TokenKey);
    }

    public Task SaveTokenAsync(string token)
        => SecureStorage.Default.SetAsync(TokenKey, token);
}

على iOS، SecureStorage يعتمد افتراضياً على Keychain مع خصائص accessibility من نوع AfterFirstUnlock. إن أردت ربط المفتاح فعلياً بالبصمة (بحيث لا يمكن قراءته حتى برمجياً بدون نجاح البصمة أولاً)، عليك النزول إلى Keychain API مباشرة واستخدام SecAccessControl مع BiometryCurrentSet. هذا يتجاوز نطاق MAUI الافتراضي ويتطلب استدعاءات P/Invoke، لكنه ضروري لتطبيقات المستوى المصرفي.

على Android، النمط المكافئ هو مفتاح Keystore مع setUserAuthenticationRequired(true). عند التوليد، مفتاح AES يُقيّد بحيث يحتاج نجاح BiometricPrompt خلال آخر 30 ثانية قبل السماح باستخدامه للتشفير أو فك التشفير. راجع توثيق BiometricPrompt الرسمي من Google للحصول على مثال CryptoObject الكامل.

معالجة الأخطاء الشائعة في الإنتاج

من واقع نشر أكثر من عشرة تطبيقات تعتمد على البصمة، هذه هي الأخطاء التي ستصادفها بلا استثناء. صدّقني، ستحدث كلها في الأسبوع الأول من الإطلاق.

الجهاز لا يدعم البصمة أصلاً

أجهزة قديمة أو نسخ Android مخصصة قد لا تحتوي مستشعر. عالج الحالة NotAvailable بعرض شاشة كلمة مرور عادية كبديل، ولا تحجب المستخدم.

المستخدم لم يسجّل أي بصمة على الجهاز

الفرق بين "غير متاح" و"غير مسجّل" جوهري. رسالة "لم تُسجّل أي بصمة على جهازك، افتح الإعدادات لإضافة واحدة" أفضل بكثير من "غير متاح". على iOS يمكنك فتح الإعدادات مباشرة:

await Launcher.OpenAsync("App-Prefs:PASSCODE");

الحظر المؤقت بعد محاولات فاشلة متكررة

iOS يقفل Face ID بعد 5 محاولات فاشلة ويطلب الرمز السري. Android يقفل لمدة 30 ثانية بعد 5 محاولات، ثم قفل دائم بعد المزيد. عالج LockedOut برسالة واضحة وعرض تسجيل دخول بكلمة مرور.

تغير البصمات المسجلة أثناء الاستخدام

إذا أضاف المستخدم بصمة جديدة أو حذف واحدة، iOS يبطل مفاتيح Keychain المربوطة بـ BiometryCurrentSet، وAndroid يبطل مفاتيح Keystore المولّدة بـ setInvalidatedByBiometricEnrollment(true). هذا سلوك أمني مقصود. يجب أن يعيد تطبيقك المستخدم إلى تسجيل الدخول الكامل عند التقاط KeyPermanentlyInvalidatedException.

اختبار المصادقة البيومترية على المحاكيات

Xcode Simulator يدعم محاكاة Face ID و Touch ID من قائمة Features → Face ID بخيارات "Enrolled" و"Matching Face" و"Non-matching Face". Android Emulator يوفر أوامر ADB مباشرة:

# نجاح بصمة برقم تعريف 1
adb -e emu finger touch 1

# فشل بصمة
adb -e emu finger touch 999

ضع خلف IBiometricService تنفيذاً وهمياً (Fake) لاختبارات الوحدة يعيد قيماً مبرمجة مسبقاً. لا تحاول محاكاة LAContext أو BiometricPrompt، فكلاهما مغلّف بواجهات أصلية لا تُختبر خارج الجهاز. اعتمد على اختبارات نهاية-إلى-نهاية على الأجهزة الفعلية عبر Appium أو توثيق Microsoft لاختبار MAUI.

وإن كنت تختبر التخزين الآمن مع أداء التطبيق، فراجع دليل تحسين أداء .NET MAUI. استدعاء Keychain في الخيط الرئيسي يبطئ فتح التطبيق بشكل ملحوظ، وقد يفسّر لك ظاهرة "التطبيق يجمد ثانية عند البداية" التي واجهتها بنفسي في أحد الإصدارات.

أسئلة شائعة

هل تحتاج .NET MAUI مكتبة خارجية للمصادقة البيومترية؟

لا تحتاجها بالضرورة. يمكن استدعاء LAContext وBiometricPrompt مباشرة عبر Bindings الأصلية في مشاريع MAUI بدون أي حزمة NuGet خارجية. مكتبات مثل Plugin.Maui.Biometric تختصر الكود لكنها تفرض قرارات تصميم قد لا تناسبك، والاختيار يعتمد على مدى تحكمك المطلوب في تجربة المستخدم.

ما الفرق بين Face ID و Touch ID في .NET MAUI؟

من ناحية الكود لا فرق، فكلاهما يُستدعى عبر نفس السياسة DeviceOwnerAuthenticationWithBiometrics. الفرق الوحيد في واجهة المستخدم: عليك قراءة LAContext.BiometryType لعرض النص والأيقونة المناسبة (وجه أو بصمة). النظام يختار المستشعر المتوفر تلقائياً.

هل يمكن استخدام البصمة كبديل لكلمة المرور بالكامل؟

لا، تقنياً وأخلاقياً. البصمة هي بوابة محلية لفك قفل بيانات مخزنة، وليست وسيلة توثيق مستقلة قابلة للنقل. يجب أن يوجد access token أو refresh token مخزّن مسبقاً في SecureStorage، والبصمة تفتح القفل عنه فقط. تسجيل الدخول الأول يبقى بكلمة مرور أو OAuth.

كيف أختبر البصمة على محاكي Android؟

سجّل بصمة وهمية أولاً من إعدادات المحاكي (Settings → Security → Fingerprint)، ثم استخدم أمر ADB: adb -e emu finger touch 1 لمحاكاة لمسة ناجحة، أو adb -e emu finger touch 999 لفشل. iOS Simulator يوفر خيارات مكافئة تحت قائمة Features → Face ID / Touch ID.

لماذا يتعطل تطبيقي عند أول محاولة استخدام Face ID؟

السبب في 99% من الحالات نسيان مفتاح NSFaceIDUsageDescription في Info.plist. iOS يقتل التطبيق فوراً كإجراء حماية للخصوصية. أضف المفتاح مع رسالة واضحة بلغة المستخدم، ثم أعد بناء التطبيق. المشكلة لا تظهر مع Touch ID، فقط Face ID.

هل تعمل المصادقة البيومترية على .NET MAUI Blazor Hybrid؟

نعم، دون أي فرق. طبقة MAUI الأصلية هي نفسها في التطبيقات التقليدية و Blazor Hybrid. تحقن IBiometricService في مكون Razor عبر @inject ثم تستدعي AuthenticateAsync من زر. الطبقة الأصلية تبقى مشتركة بغض النظر عن طبقة الواجهة.

David O'Reilly
عن الكاتب David O'Reilly

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