انتقال از FIDO2 به Credential Manager

با پشتیبانی از گذرکلیدها، ورود به سیستم فدرال، و ارائه‌دهندگان اصالت‌سنجی طرف سوم، Credential Manager میانای برنامه‌سازی کاربردی توصیه‌شده برای اصالت‌سنجی در Android است که محیطی امن و راحت را فراهم می‌کند که به کاربران اجازه می‌دهد اطلاعات کاربری خود را همگام‌سازی و مدیریت کنند. توسعه‌دهندگانی که از اعتبارنامه‌های محلی FIDO2 استفاده می‌کنند باید برنامه خود را به‌روز کنند تا با ادغام شدن با Credential Manager API از اصالت‌سنجی گذرکلید پشتیبانی کند. این سند نحوه انتقال پروژه از FIDO2 به Credential Manager را شرح می‌دهد.

دلایل انتقال از FIDO2 به Credential Manager

در اکثر موارد، باید ارائه‌دهنده اصالت‌سنجی برنامه Android خود را به Credential Manager منتقل کنید. دلایل انتقال به «مدیر اطلاعات کاربری» عبارت‌اند از:

  • پشتیبانی از گذرکلید: Credential Manager از گذرکلید پشتیبانی می‌کند، یک سازوکار اصالت‌سنجی جدید و بدون گذرواژه که ایمن‌تر و آسان‌تر از گذرواژه است.
  • روش‌های ورود به سیستم چندگانه: Credential Manager از چندین روش ورود به سیستم، ازجمله گذرواژه، گذرکلید، و روش‌های ورود به سیستم فدرالی پشتیبانی می‌کند. این کار اصالت‌سنجی کاربران در برنامه شما را، صرف‌نظر از روش اصالت‌سنجی ترجیحی آن‌ها، آسان‌تر می‌کند.
  • پشتیبانی از ارائه‌دهنده اطلاعات کاربری طرف سوم: در Android 14 و بالاتر، «مدیر اطلاعات کاربری» از چندین ارائه‌دهنده اطلاعات کاربری طرف سوم پشتیبانی می‌کند. این یعنی کاربران شما می‌توانند از اعتبارنامه‌های موجود خود از ارائه‌دهندگان دیگر برای ورود به سیستم برنامه شما استفاده کنند.
  • تجربه کاربری یکپارچه: «مدیر اطلاعات اعتباری» تجربه کاربری یکپارچه‌تری برای اصالت‌سنجی در برنامه‌ها و سازوکارهای ورود به سیستم ارائه می‌دهد. این کار باعث می‌شود کاربران راحت‌تر جریان اصالت‌سنجی برنامه‌تان را درک و از آن استفاده کنند.

برای شروع انتقال از FIDO2 به Credential Manager، مراحل زیر را دنبال کنید.

به‌روزرسانی وابستگی‌ها

  1. افزایه Kotlin را در build.gradle پروژه‌تان به نسخه ۱.۸.۱۰ یا بالاتر به‌روز کنید.

      plugins {
        //…
          id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
        //…
      }
    
  2. در build.gradle پروژه‌تان، وابستگی‌هایتان را به‌روز کنید تا از جدیدترین نسخه‌های Credential Manager و کتابخانه‌های «اصالت‌سنجی خدمات Play» استفاده کنید.

      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 را با مقداردهی اولیه «مدیر اطلاعات کاربری» جایگزین کنید. این اعلامیه را در کلاسی که برای روش‌های ایجاد گذرکلید و ورود به سیستم استفاده می‌کنید اضافه کنید:

    val credMan = CredentialManager.create(context)
    

ایجاد گذرکلید

باید گذرکلید جدیدی ایجاد کنید، آن را با حساب کاربر مرتبط کنید، و کلید عمومی گذرکلید را در سرورتان ذخیره کنید تا کاربر بتواند با آن به سیستم وارد شود. با به‌روزرسانی فراخوانی‌های تابع ثبت، برنامه‌تان را با این توانایی راه‌اندازی کنید.

شکل ۱. این شکل نشان می‌دهد که وقتی گذرکلیدی بااستفاده از «مدیر اطلاعات کاربری» ایجاد می‌شود، داده‌ها چگونه بین برنامه و سرور ردوبدل می‌شوند.
  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 به‌روزرسانی می‌کنید و تابعی را برای اصالت‌سنجی ازطریق گذرکلیدها پیاده‌سازی می‌کنید.

شکل ۲. جریان اصالت‌سنجی گذرکلید 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 است، بنابراین هیچ تغییری لازم نیست.

منابع بیشتر