הטמעה של אימות כתובת אימייל באמצעות Digital Credentials API

במדריך הזה מוסבר איך להטמיע אחזור של כתובות אימייל מאומתות באמצעות Digital Credentials Verifier API דרך בקשת OpenID for Verifiable Presentations ‏ (OpenID4VP).

הוספת יחסי תלות

בקובץ build.gradle של האפליקציה, מוסיפים את יחסי התלות הבאים עבור Credential Manager:

Kotlin

dependencies {
    implementation("androidx.credentials:credentials:1.7.0-alpha01")
    implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha01")
}

Groovy

dependencies {
    implementation "androidx.credentials:credentials:1.7.0-alpha01"
    implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha01"
}

הפעלת Credential Manager

משתמשים בהקשר של האפליקציה או הפעילות כדי ליצור אובייקט CredentialManager.

// Use your app or activity context to instantiate a client instance of
// CredentialManager.
private val credentialManager = CredentialManager.create(context)

יצירת בקשה לפרטי כניסה דיגיטליים

כדי לבקש אימות של כתובת אימייל, צריך ליצור GetCredentialRequest שמכיל GetDigitalCredentialOption. האפשרות הזו דורשת requestJsonמחרוזת בפורמט של בקשת OpenID ל-Verifiable Presentations ‏ (OpenID4VP).

בקשת ה-JSON של OpenID4VP חייבת להיות במבנה מסוים. הספקים הנוכחיים תומכים במבנה JSON עם עטיפה חיצונית של "digital": {"requests": [...]}.

        val nonce = generateSecureRandomNonce()

// This request follows the OpenID4VP spec
        val openId4vpRequest = """
    {
      "requests": [
        {
          "protocol": "openid4vp-v1-unsigned",
          "data": {
            "response_type": "vp_token",
            "response_mode": "dc_api",
            "nonce": "$nonce",
            "dcql_query": {
              "credentials": [
                {
                  "id": "user_info_query",
                  "format": "dc+sd-jwt",
                   "meta": { 
                      "vct_values": ["UserInfoCredential"] 
                   },
                  "claims": [ 
                    {"path": ["email"]}, 
                    {"path": ["name"]},  
                    {"path": ["given_name"]},
                    {"path": ["family_name"]},
                    {"path": ["picture"]},
                    {"path": ["hd"]},
                    {"path": ["email_verified"]}
                  ]
                }
              ]
            }
          }
        }
      ]
    }
    """

        val getDigitalCredentialOption = GetDigitalCredentialOption(requestJson = openId4vpRequest)
        val request = GetCredentialRequest(listOf(getDigitalCredentialOption))

הבקשה מכילה את הפרטים העיקריים הבאים:

  • שאילתת DCQL: ב-dcql_query מצוין סוג האישורים והטענות המבוקשות (email_verified). אפשר לבקש טענות אחרות כדי לקבוע את רמת האימות. הנה כמה דוגמאות לתלונות אפשריות:

    • email_verified: בתשובה, זהו ערך בוליאני שמציין אם האימייל אומת.
    • hd (דומיין מתארח): התשובה ריקה.
  • אם כתובת האימייל לא מסתיימת ב-‎@gmail.com, ‏ Google אימתה את כתובת האימייל הזו כשנוצר חשבון Google, אבל אין טענה לגבי עדכניות. לכן, כשמדובר בכתובות אימייל שאינן של Google, כדאי להשתמש באתגר נוסף, כמו קוד OTP, כדי לאמת את המשתמש. כדי להבין את הסכימה של פרטי הכניסה ואת הכללים הספציפיים לאימות שדות כמו email_verified, אפשר לעיין במדריכים של Google Identity.

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

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

לאחר מכן, עוטפים את ה-JSON של openId4vpRequest ב-GetDigitalCredentialOption, יוצרים GetCredentialRequest וקוראים ל-getCredential().

הצגת הבקשה למשתמש

הצגת הבקשה למשתמש באמצעות ממשק המשתמש המובנה של Credential Manager.

try {
    // Requesting Digital Credential from user...
    val result = credentialManager.getCredential(activity, request)

    when (val credential = result.credential) {
        is DigitalCredential -> {
            val responseJsonString = credential.credentialJson

            // Successfully received digital credential response.

            // Next, parse this response and send it to your server.
            // ...
        }

        else -> {
            // handle Unexpected State() - Up to the developer
        }
    }
} catch (e: Exception) {
    // handle exceptions - Up to the developer
}

ניתוח התגובה בלקוח

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

הקוד הבא מחלץ את ה-JWT של גילוי סלקטיבי (SD-JWT) הגולמי ומשתמש בפונקציית עזר כדי לפענח את ההצהרות שלו.

// 1. Parse the outer JSON wrapper to get the `vp_token`
val responseData = JSONObject(responseJsonString)
val vpToken = responseData.getJSONObject("vp_token")

// 2. Extract the raw SD-JWT string
val credentialId = vpToken.keys().next()
val rawSdJwt = vpToken.getJSONArray(credentialId).getString(0)

// 3. Use your parser to get the verified claims
// Server-side validation/parsing is highly recommended.

// Assumes a local parser like the one in our SdJwtParser.kt sample
val claims = SdJwtParser.parse(rawSdJwt)
Log.d("TAG", "Parsed Claims: ${claims.toString(2)}")

// 4. Create your VerifiedUserInfo object with REAL data
val userInfo = VerifiedUserInfo(
    email = claims.getString("email"),
    displayName = claims.optString("name", claims.getString("email"))
)

טיפול בתגובה

ממשק Credential Manager API יחזיר תגובה מסוג DigitalCredential.

בדוגמה הבאה אפשר לראות איך נראה responseJsonString הגולמי, ואיך נראות הטענות אחרי ניתוח ה-SD-JWT הפנימי שבו מקבלים גם מטא-נתונים נוספים וגם אימייל מאומת:

/*
// Example of the raw JSON response from credential.credentialJson:
{
  "vp_token": {
    // This key matches the 'id' you set in your dcql_query
    "user_info_query": [
      // The SD-JWT string (Issuer JWT ~ Disclosures ~ Key Binding JWT)
      "eyJhbGciOiJ...~WyI...IiwgImVtYWlsIiwgInVzZXJAZXhhbXBsZS5jb20iXQ~...~eyJhbGciOiJ..."
    ]
  }
}

// Example of the parsed and verified claims from the SD-JWT on your server:
{
  "cnf": {
    "jwk": {..}
  },
  "exp": 1775688222,
  "iat": 1775083422,
  "iss": "https://verifiablecredentials-pa.googleapis.com",
  "vct": "UserInfoCredential",
  "email": "[email protected]",
  "email_verified": true,
  "given_name": "Jane",
  "family_name": "Doe",
  "name": "Jane Doe",
  "picture": "http://example.com/janedoe/me.jpg",
  "hd": ""
}
 */

אימות בצד השרת ליצירת חשבון

מכיוון שכתובת האימייל שאוחזרה מאומתת באמצעות הצפנה, אפשר לדלג על שלב האימות של קוד ה-OTP שמתקבל באימייל. כך מצמצמים באופן משמעותי את החיכוך בתהליך ההרשמה, ויכול להיות שגם מגדילים את שיעור ההמרה. הדרך הטובה ביותר לטפל בתהליך הזה היא בשרת שלכם. הלקוח שולח את התגובה הגולמית (שמכילה את vp_token) ואת ה-nonce המקורי לנקודת קצה חדשה בשרת.

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

האישור הדיגיטלי מספק שתי רמות קריטיות של אימות לשרת שלכם:

  • אותנטיות הנתונים: אימות כתובת האתר של הגורם המנפיק (iss) והחתימה SD-JWT מוכיח שרשות מהימנה הנפיקה את הנתונים האלה.
  • זהות המציג: אימות השדה cnf וחתימת הקישור למפתח (kb) מאשר שהאישורים משותפים על ידי אותו מכשיר שהם הונפקו לו במקור, וכך מונעים את היירוט שלהם או את השימוש בהם במכשיר אחר.

האימות בשרת צריך לכלול את הפעולות הבאות:

  • אימות המנפיק: מוודאים שהשדה iss (מנפיק) תואם לערך https://verifiablecredentials-pa.googleapis.com.
  • אימות החתימה: בודקים את החתימה של ה-SD-JWT באמצעות המפתחות הציבוריים (JWK) שזמינים בכתובת https://verifiablecredentials-pa.googleapis.com/.well-known/vc-public-jwks.

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

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

try {
    // Send the raw credential response and the original nonce to your server.
    // Your server must validate the response. createAccountWithVerifiedCredentials
    // is a custom implementation per each RP for server side verification and account creation.
    val serverResponse = createAccountWithVerifiedCredentials(responseJsonString, nonce)

    // Server returns the new account info (e.g., email, name)
    val claims = JSONObject(serverResponse.json)

    val userInfo = VerifiedUserInfo(
        email = claims.getString("email"),
        displayName = claims.optString("name", claims.getString("email"))
    )

    // handle response - Up to the developer
} catch (e: Exception) {
    // handle exceptions - Up to the developer
}

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

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

תמיכה ב-WebView

כדי שהתהליך יפעל ב-WebView, מפתחים צריכים להטמיע גשר JavaScript (JS Bridge) כדי לאפשר את ההעברה. הגשר הזה מאפשר ל-WebView לשלוח אות לאפליקציית נייטיב, ואז האפליקציה יכולה לבצע את הקריאה בפועל ל-Credential Manager API.

למידע נוסף