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

‫Credential Manager הוא ה-API המומלץ לאימות ב-Android, והוא תומך במפתחות גישה, בכניסה מאוחדת ובספקי אימות של צד שלישי. הוא מספק סביבה מאובטחת ונוחה שמאפשרת למשתמשים לסנכרן ולנהל את פרטי הכניסה שלהם. מפתחים שמשתמשים בפרטי כניסה מקומיים של FIDO2 צריכים לעדכן את האפליקציה שלהם כדי לתמוך באימות באמצעות מפתח גישה, על ידי שילוב עם Credential Manager API. במאמר הזה מוסבר איך להעביר את הפרויקט מ-FIDO2 ל'מנהל פרטי הכניסה'.

סיבות למעבר מ-FIDO2 למרכז ניהול האישורים

ברוב המקרים, כדאי להעביר את ספק האימות של אפליקציית Android אל מנהל פרטי הכניסה. הסיבות למעבר אל Credential Manager כוללות:

  • תמיכה במפתחות גישה: מנהל פרטי הכניסה תומך במפתחות גישה, מנגנון אימות חדש ללא סיסמה שהוא מאובטח יותר וקל יותר לשימוש מסיסמאות.
  • שיטות כניסה מרובות: מנהל פרטי הכניסה תומך בשיטות כניסה מרובות, כולל סיסמאות, מפתחות גישה ושיטות כניסה משולבות. כך קל יותר למשתמשים לבצע אימות באפליקציה, בלי קשר לשיטת האימות המועדפת שלהם.
  • תמיכה בספקי אישורים של צד שלישי: ב-Android 14 ואילך, מרכז ניהול האישורים תומך בכמה ספקי אישורים של צד שלישי. המשמעות היא שהמשתמשים יכולים להשתמש בפרטי הכניסה הקיימים שלהם מספקים אחרים כדי להיכנס לאפליקציה שלכם.
  • חוויית משתמש עקבית: 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 של הפרויקט, מעדכנים את יחסי התלות כדי להשתמש בגרסאות העדכניות של מנהל פרטי הכניסה ושל ספריות האימות של שירותי 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 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() {
        // ...
        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. קוראים לשיטת ה-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, ולכן לא נדרשים שינויים.

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

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

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

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