מעבר מ-FIDO2 למנהל פרטי הכניסה

Credential Manager הוא ה-API המומלץ לאימות ב-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 בפרויקט, לגרסה 1.8.10 ואילך.

    plugins {
      //…
        id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
      //…
    }
    
  2. ב-build.gradle של הפרויקט, מעדכנים את יחסי התלות כך שישתמשו ב-Credential Manager ובאימות של Play Services.

    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 below:
      implementation 'androidx.credentials:credentials-play-services-auth:<latest-version>'
      // ...
    }
    
  3. מחליפים את האיפוס של FIDO באיפוס של Credential Manager. מוסיפים את ההצהרה הזו לכיתה שבה משתמשים ליצירת מפתחות גישה ולשיטות כניסה:

    val credMan = CredentialManager.create(context)
    

יצירת מפתחות גישה

כדי שהמשתמשים יוכלו להיכנס באמצעות מפתח הגישה, תצטרכו ליצור מפתח גישה חדש, לשייך אותו לחשבון של משתמש ולאחסן את המפתח הציבורי של מפתח הגישה בשרת. כדי להגדיר את היכולת הזו באפליקציה, צריך לעדכן את קריאות הפונקציה register.

איור 1. באיור הזה מוצגת החלפת הנתונים בין האפליקציה לשרת כשיוצרים מפתח גישה באמצעות Credential Manager.
  1. כדי לקבל את הפרמטרים הנדרשים שנשלחים לשיטה createCredential() במהלך יצירת מפתח הגישה, מוסיפים את name("residentKey").value("required") (כפי שמתואר במפרט WebAuthn) לקריאה לשרת registerRequest().

    suspend fun registerRequest(sessionId: String ... {
        // ...
        .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(
            Request.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. קריאה לשיטת ה-API createCredential().

    suspend fun createPasskey(
        activity: Activity,
        requestResult: JSONObject
        ): CreatePublicKeyCredentialResponse? {
            val request = CreatePublicKeyCredentialRequest(requestResult.toString())
            var response: CreatePublicKeyCredentialResponse? = null
            try {
                response = credMan.createCredential(
                    request as CreateCredentialRequest,
                    activity
                ) as CreatePublicKeyCredentialResponse
            } catch (e: CreateCredentialException) {
    
                showErrorAlert(activity, e)
    
                return null
            }
            return response
        }
    
  5. אחרי שהקריאה מסתיימת בהצלחה, שולחים את התשובה חזרה לשרת. הבקשה והתגובה לשיחה הזו דומים להטמעת FIDO2, כך שאין צורך בשינויים.

אימות באמצעות מפתחות גישה

אחרי שתגדירו את האפשרות ליצור מפתחות גישה, תוכלו להגדיר את האפליקציה כך שתאפשר למשתמשים להיכנס ולבצע אימות באמצעות מפתחות הגישה שלהם. כדי לעשות זאת, צריך לעדכן את קוד האימות כדי לטפל בתוצאות של מנהל פרטי הכניסה, ולהטמיע פונקציה לאימות באמצעות מפתחות גישה.

איור 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. מסירים בבטחה מהתצוגה שלכם את כל השיטות שמטפלות בקריאות של מרכז ה-Intent ובקריאות של תוצאות הפעילות.

  4. מכיוון ש-signInRequest() מחזיר עכשיו JSONObject, אין צורך ליצור PendingIntent. מחליפים את ה-Intent שהוחזר ב-JSONObject, וקוראים ל-getCredential() מה-methods של ה-API.

    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. אחרי שהקריאה מסתיימת בהצלחה, שולחים את התשובה חזרה לשרת כדי לאמת את המשתמש. הפרמטרים של הבקשה והתגובה בקריאה הזו ל-API דומים לאלה של הטמעת FIDO2, כך שאין צורך לבצע שינויים.

מקורות מידע נוספים