ใช้การยืนยันอีเมลด้วย Digital Credentials API

คู่มือนี้อธิบายวิธีใช้การดึงข้อมูลอีเมลที่ยืนยันแล้วโดยใช้ Digital Credentials Verifier API ผ่านคำขอ OpenID for Verifiable Presentations (OpenID4VP)

เพิ่มทรัพยากร Dependency

ในไฟล์ build.gradle ของแอป ให้เพิ่มทรัพยากร Dependency ต่อไปนี้สำหรับ 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 สำหรับการนำเสนอที่ตรวจสอบได้ (OpenID4VP)

JSON ของคำขอ OpenID4VP ต้องเป็นไปตามโครงสร้างที่เฉพาะเจาะจง ผู้ให้บริการปัจจุบัน รองรับโครงสร้าง JSON ที่มี Wrapper "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

  • nonce: ระบบจะสร้างค่าแบบสุ่มที่ไม่ซ้ำกันและปลอดภัยด้วยการเข้ารหัสสำหรับคำขอแต่ละรายการ ซึ่งมีความสำคัญอย่างยิ่งต่อความปลอดภัย เนื่องจากจะป้องกันการโจมตีแบบรีเพลย์

  • UserInfoCredential: ค่านี้หมายถึงข้อมูลประจำตัวดิจิทัลประเภทหนึ่งที่เฉพาะเจาะจง ซึ่งมีแอตทริบิวต์ของผู้ใช้ การระบุข้อมูลนี้ในคำขอเป็น สิ่งสำคัญในการแยกแยะกรณีการใช้งานการยืนยันทางอีเมล

จากนั้นให้ห่อ openId4vpRequest JSON ใน GetDigitalCredentialOption สร้าง GetCredentialRequest แล้วเรียกใช้ getCredential()

แสดงคำขอต่อผู้ใช้

แสดงคำขอต่อผู้ใช้โดยใช้ UI ในตัวของ 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
}

แยกวิเคราะห์การตอบกลับในไคลเอ็นต์

หลังจากได้รับคำตอบแล้ว คุณจะทำการแยกวิเคราะห์เบื้องต้นในไคลเอ็นต์ได้ ซึ่งมีประโยชน์ในการอัปเดต UI ทันที เช่น การแสดง ชื่อผู้ใช้

โค้ดต่อไปนี้จะดึง 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": "jane.doe.246745@gmail.com",
  "email_verified": true,
  "given_name": "Jane",
  "family_name": "Doe",
  "name": "Jane Doe",
  "picture": "http://example.com/janedoe/me.jpg",
  "hd": ""
}
 */

การตรวจสอบฝั่งเซิร์ฟเวอร์สำหรับการสร้างบัญชี

เนื่องจากอีเมลที่ดึงมาได้รับการยืนยันด้วยการเข้ารหัส คุณจึงข้ามขั้นตอนการยืนยันทางอีเมล OTP ได้ ซึ่งจะช่วยลดอุปสรรคในการลงชื่อสมัครใช้และอาจ เพิ่ม Conversion ได้อย่างมาก กระบวนการนี้ควรจัดการในเซิร์ฟเวอร์ของคุณ ไคลเอ็นต์ ส่งการตอบกลับดิบ (ที่มี vp_token) และ Nonce เดิมไปยัง ปลายทางเซิร์ฟเวอร์ใหม่

สำหรับการยืนยัน แอปพลิเคชันของคุณต้องส่ง responseJsonString แบบเต็มไปยังเซิร์ฟเวอร์เพื่อการตรวจสอบด้วยการเข้ารหัสก่อนสร้างบัญชีหรือบันทึกการเข้าสู่ระบบของผู้ใช้

ข้อมูลเข้าสู่ระบบดิจิทัลมีการยืนยัน 2 ระดับที่สำคัญสำหรับเซิร์ฟเวอร์ของคุณ ดังนี้

  • ความถูกต้องของข้อมูล: การยืนยัน URL ของผู้ออก (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 จริงได้

ดูเพิ่มเติม