نقل البيانات من FIDO2 إلى "مدير بيانات الاعتماد"

من خلال إتاحة استخدام مفاتيح المرور وتسجيل الدخول الموحّد ومقدّمي خدمات المصادقة التابعين لجهات خارجية، تُعدّ واجهة برمجة التطبيقات Credential Manager هي الواجهة المقترَحة للمصادقة على Android، إذ توفّر بيئة آمنة ومريحة تتيح للمستخدمين مزامنة بيانات اعتمادهم وإدارتها. بالنسبة إلى المطوّرين الذين يستخدمون بيانات اعتماد FIDO2 المحلية، يجب تحديث التطبيق لإتاحة المصادقة باستخدام مفتاح المرور من خلال الدمج مع واجهة Credential Manager API. يوضّح هذا المستند كيفية نقل مشروعك من FIDO2 إلى Credential Manager.

أسباب نقل البيانات من FIDO2 إلى Credential Manager

في معظم الحالات، عليك نقل مزوّد خدمة المصادقة في تطبيق Android إلى Credential Manager. تشمل أسباب نقل البيانات إلى Credential Manager ما يلي:

  • إتاحة مفاتيح المرور: تتيح واجهة برمجة التطبيقات Credential Manager استخدام مفاتيح المرور، وهي آلية مصادقة جديدة لا تتطلّب كلمات مرور وتتميّز بأنّها أكثر أمانًا وسهولة في الاستخدام من كلمات المرور.
  • طُرق متعدّدة لتسجيل الدخول: يتيح Credential Manager استخدام طُرق متعدّدة لتسجيل الدخول، بما في ذلك كلمات المرور ومفاتيح المرور وطُرق تسجيل الدخول الموحّد. ويسهّل ذلك على المستخدمين المصادقة على تطبيقك، بغض النظر عن طريقة المصادقة المفضّلة لديهم.
  • إتاحة استخدام مقدّمي خدمات بيانات الاعتماد التابعين لجهات خارجية: في الإصدار 14 من نظام التشغيل Android والإصدارات الأحدث، يتيح Credential Manager استخدام العديد من مقدّمي خدمات بيانات الاعتماد التابعين لجهات خارجية. وهذا يعني أنّه يمكن للمستخدمين استخدام بيانات اعتمادهم الحالية من مقدّمي خدمات آخرين لتسجيل الدخول إلى تطبيقك.
  • تجربة مستخدم متّسقة: يوفّر Credential Manager تجربة مستخدم أكثر اتساقًا للمصادقة على مستوى التطبيقات وآليات تسجيل الدخول. ويسهّل ذلك على المستخدمين فهم مسار المصادقة في تطبيقك واستخدامه.

لبدء عملية نقل البيانات من FIDO2 إلى Credential Manager، اتّبِع الخطوات التالية.

تعديل التبعيات

  1. يُرجى تحديث المكوّن الإضافي Kotlin في ملف build.gradle الخاص بمشروعك إلى الإصدار 1.8.10 أو إصدار أحدث.

      plugins {
        //…
          id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
        //…
      }
    
  2. في ملف build.gradle الخاص بمشروعك، حدِّث التبعيات لاستخدام أحدث إصدارات مكتبات Credential Manager وخدمات Play Authentication.

      dependencies {
        // ...
        // Credential Manager:
        implementation 'androidx.credentials:credentials:<latest-version>'
    
        // Play Services Authentication:
        // Optional - needed for credentials support from play services, for devices running
        // Android 13 and lower:
        implementation 'androidx.credentials:credentials-play-services-auth:<latest-version>'
        // ...
      }
    
  3. استبدِل عملية إعداد FIDO بعملية إعداد Credential Manager. أضِف هذا التعريف في الفئة التي تستخدمها لإنشاء مفتاح المرور وطرق تسجيل الدخول:

    val credMan = CredentialManager.create(context)
    

إنشاء مفاتيح مرور

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

الشكل 1. يوضّح هذا الشكل كيفية تبادل البيانات بين التطبيق والخادم عند إنشاء مفتاح مرور باستخدام &quot;إدارة بيانات الاعتماد&quot;.
  1. للحصول على المَعلمات اللازمة التي يتم إرسالها إلى طريقة createCredential() أثناء إنشاء مفتاح المرور، أضِف name("residentKey").value("required") (كما هو موضّح في مواصفات WebAuthn) إلى طلب خادم registerRequest().

    suspend fun registerRequest() {
        // ...
        val call = client.newCall(
            Builder()
                .method(
                    "POST",
                    jsonRequestBody {
                        name("attestation").value("none")
                        name("authenticatorSelection").objectValue {
                            name("residentKey").value("required")
                        }
                    }
                ).build()
        )
        // ...
    }
    
  2. اضبط نوع return لكل من registerRequest() وجميع الدوال الفرعية على JSONObject.

    suspend fun registerRequest(sessionId: String): ApiResult<JSONObject> {
        val call = client.newCall(
            Builder()
                .url("$BASE_URL/<your api url>")
                .addHeader("Cookie", formatCookie(sessionId))
                .method(
                    "POST",
                    jsonRequestBody {
                        name("attestation").value("none")
                        name("authenticatorSelection").objectValue {
                            name("authenticatorAttachment").value("platform")
                            name("userVerification").value("required")
                            name("residentKey").value("required")
                        }
                    }
                ).build()
        )
        val response = call.await()
        return response.result("Error calling the api") {
            parsePublicKeyCredentialCreationOptions(
                body ?: throw ApiException("Empty response from the api call")
            )
        }
    }
    
  3. يمكنك إزالة أي طرق تتعامل مع طلبات مشغّل الأهداف ونتائج الأنشطة من العرض بأمان.

  4. بما أنّ registerRequest() يعرض الآن JSONObject، لن تحتاج إلى إنشاء PendingIntent. استبدِل الغرض الذي تم عرضه بـ JSONObject. يجب تعديل طلبات مشغّل الأهداف لاستدعاء createCredential() من واجهة برمجة التطبيقات Credential Manager API. استدعاء طريقة واجهة برمجة التطبيقات createCredential()

    suspend fun createPasskey(
        activity: Activity,
        requestResult: JSONObject
    ): CreatePublicKeyCredentialResponse? {
        val request = CreatePublicKeyCredentialRequest(requestResult.toString())
        var response: CreatePublicKeyCredentialResponse? = null
        try {
            response = credMan.createCredential(
                request = request as CreateCredentialRequest,
                context = activity
            ) as CreatePublicKeyCredentialResponse
        } catch (e: CreateCredentialException) {
    
            showErrorAlert(activity, e)
    
            return null
        }
        return response
    }
    
  5. بعد نجاح الطلب، أرسِل الردّ إلى الخادم. يتشابه الطلب والاستجابة لهذا الاستدعاء مع تنفيذ FIDO2، لذا لا يلزم إجراء أي تغييرات.

المصادقة باستخدام مفاتيح المرور

بعد إعداد عملية إنشاء مفاتيح المرور، يمكنك إعداد تطبيقك للسماح للمستخدمين بتسجيل الدخول والمصادقة باستخدام مفاتيح المرور الخاصة بهم. لإجراء ذلك، عليك تعديل رمز المصادقة لمعالجة نتائج Credential Manager وتنفيذ دالة للمصادقة من خلال مفاتيح المرور.

الشكل 2. سير عملية المصادقة باستخدام مفتاح المرور في Credential Manager
  1. إنّ طلب تسجيل الدخول الذي يتم إرساله إلى الخادم للحصول على المعلومات اللازمة لإرسالها إلى getCredential() هو نفسه طلب تنفيذ FIDO2. ولا يلزم إجراء أي تغييرات.
  2. على غرار طلب التسجيل، يكون الردّ الذي يتم عرضه بتنسيق JSONObject.

    /**
     * @param sessionId The session ID to be used for the sign-in.
     * @param credentialId The credential ID of this device.
     * @return a JSON object.
     */
    suspend fun signinRequest(): ApiResult<JSONObject> {
        val call = client.newCall(
            Builder().url(
                buildString {
                    append("$BASE_URL/signinRequest")
                }
            ).method("POST", jsonRequestBody {})
                .build()
        )
        val response = call.await()
        return response.result("Error calling /signinRequest") {
            parsePublicKeyCredentialRequestOptions(
                body ?: throw ApiException("Empty response from /signinRequest")
            )
        }
    }
    
    /**
     * @param sessionId The session ID to be used for the sign-in.
     * @param response The JSONObject for signInResponse.
     * @param credentialId id/rawId.
     * @return A list of all the credentials registered on the server,
     * including the newly-registered one.
     */
    suspend fun signinResponse(
        sessionId: String,
        response: JSONObject,
        credentialId: String
    ): ApiResult<Unit> {
    
        val call = client.newCall(
            Builder().url("$BASE_URL/signinResponse")
                .addHeader("Cookie", formatCookie(sessionId))
                .method(
                    "POST",
                    jsonRequestBody {
                        name("id").value(credentialId)
                        name("type").value(PUBLIC_KEY.toString())
                        name("rawId").value(credentialId)
                        name("response").objectValue {
                            name("clientDataJSON").value(
                                response.getString("clientDataJSON")
                            )
                            name("authenticatorData").value(
                                response.getString("authenticatorData")
                            )
                            name("signature").value(
                                response.getString("signature")
                            )
                            name("userHandle").value(
                                response.getString("userHandle")
                            )
                        }
                    }
                ).build()
        )
        val apiResponse = call.await()
        return apiResponse.result("Error calling /signingResponse") {
        }
    }
    
  3. يمكنك إزالة أي طرق تتعامل مع مشغّل الأهداف ونتائج الأنشطة من العرض بأمان.

  4. بما أنّ signInRequest() يعرض الآن JSONObject، لن تحتاج إلى إنشاء PendingIntent. استبدِل الغرض الذي تم عرضه بـ JSONObject، واستدعِ getCredential() من طرق واجهة برمجة التطبيقات.

    suspend fun getPasskey(
        activity: Activity,
        creationResult: JSONObject
    ): GetCredentialResponse? {
        Toast.makeText(
            activity,
            "Fetching previously stored credentials",
            Toast.LENGTH_SHORT
        )
            .show()
        var result: GetCredentialResponse? = null
        try {
            val request = GetCredentialRequest(
                listOf(
                    GetPublicKeyCredentialOption(
                        creationResult.toString(),
                        null
                    ),
                    GetPasswordOption()
                )
            )
            result = credMan.getCredential(activity, request)
            if (result.credential is PublicKeyCredential) {
                val publicKeycredential = result.credential as PublicKeyCredential
                Log.i("TAG", "Passkey ${publicKeycredential.authenticationResponseJson}")
                return result
            }
        } catch (e: Exception) {
            showErrorAlert(activity, e)
        }
        return result
    }
    
  5. بعد نجاح الطلب، أرسِل الردّ إلى الخادم للتحقّق من صحة المستخدم ومصادقته. تتشابه مَعلمات الطلب والاستجابة لطلب البيانات من واجهة برمجة التطبيقات هذا مع عملية تنفيذ FIDO2، لذا لا يلزم إجراء أي تغييرات.

مراجع إضافية