แสดงข้อมูลเข้าสู่ระบบด้วยแอปผู้ถือ

Credential Manager Holder API ช่วยให้แอปผู้ถือ (หรือที่เรียกว่า "Wallet") ของ Android จัดการและแสดงข้อมูลเข้าสู่ระบบดิจิทัลต่อผู้ตรวจสอบได้ คู่มือนี้อธิบายวิธีลงทะเบียนข้อมูลเข้าสู่ระบบของแอปผู้ถือด้วย Credential Manager และวิธีจัดการคำขอการนำเสนอเมื่อผู้ใช้เลือกข้อมูลเข้าสู่ระบบ Holder API รองรับใน Android 6 (ระดับ API 23) ขึ้นไป

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

หากต้องการใช้ Credential Manager Holder API ให้เพิ่มทรัพยากร Dependency ต่อไปนี้ลงในสคริปต์ build.gradle ของโมดูลแอป

dependencies {
    // Used to implement credential registries
    implementation "androidx.credentials.registry:registry-digitalcredentials-mdoc:1.0.0-alpha05"
    implementation "androidx.credentials.registry:registry-digitalcredentials-openid:1.0.0-alpha05"
    implementation "androidx.credentials.registry:registry-digitalcredentials-sdjwtvc:1.0.0-alpha05"
    implementation "androidx.credentials.registry:registry-provider:1.0.0-alpha05"
    implementation "androidx.credentials.registry:registry-provider-play-services:1.0.0-alpha05"
}

ลงทะเบียนข้อมูลเข้าสู่ระบบ

ก่อนอื่น ให้สร้างอินสแตนซ์ RegistryManager ที่คุณจะใช้เพื่อลงทะเบียนคำขอ OpenId4VpRegistry

// Create the registry manager
val registryManager = RegistryManager.create(context)

สร้างคำขอ OpenId4VpRegistry

คุณต้องลงทะเบียน OpenId4VpRegistry เพื่อจัดการคำขอ OpenID4VP จากผู้ตรวจสอบ สมมติว่าคุณมีประเภทข้อมูลในเครื่องที่โหลดด้วยข้อมูลเข้าสู่ระบบ Wallet (เช่น sdJwtsFromStorage และ mdocsFromStorage) คุณต้องแปลงประเภทข้อมูลดังกล่าวเป็นประเภทข้อมูล Jetpack DigitalCredentialEntry ที่เทียบเท่ากัน ซึ่งก็คือ SdJwtEntry และ MdocEntry

เพิ่ม SD-JWT ลงในรีจิสทรี

แมปข้อมูลเข้าสู่ระบบ SD-JWT ในเครื่องแต่ละรายการกับ SdJwtEntry สำหรับรีจิสทรี ดังนี้

fun mapToSdJwtEntries(sdJwtsFromStorage: List<StoredSdJwtEntry>): List<SdJwtEntry> {
    val list = mutableListOf<SdJwtEntry>()

    for (sdJwt in sdJwtsFromStorage) {
        list.add(
            SdJwtEntry(
                verifiableCredentialType = sdJwt.getVCT(),
                claims = sdJwt.getClaimsList(),
                entryDisplayPropertySet = sdJwt.toDisplayProperties(),
                id = sdJwt.getId() // Make sure this cannot be readily guessed
            )
        )
    }
    return list
}

เพิ่ม mdoc ลงใน Registry

แมปข้อมูลเข้าสู่ระบบ mdoc ในเครื่องกับประเภท Jetpack MdocEntry:

fun mapToMdocEntries(mdocsFromStorage: List<StoredMdocEntry>): List<MdocEntry> {
    val list = mutableListOf<MdocEntry>()

    for (mdoc in mdocsFromStorage) {
        list.add(
            MdocEntry(
                docType = mdoc.retrieveDocType(),
                fields = mdoc.getFields(),
                entryDisplayPropertySet = mdoc.toDisplayProperties(),
                id = mdoc.getId() // Make sure this cannot be readily guessed
            )
        )
    }
    return list
}

ประเด็นสำคัญเกี่ยวกับโค้ด

  • รหัสที่เข้ารหัส: วิธีหนึ่งในการกำหนดค่าช่อง ID คือการลงทะเบียนตัวระบุข้อมูลเข้าสู่ระบบที่เข้ารหัส เพื่อให้มีเพียงคุณเท่านั้นที่ถอดรหัสค่าได้
  • การแปลภาษา: ช่องแสดงผล UI สำหรับทั้ง 2 รูปแบบควรได้รับการแปลภาษา

รวมและลงทะเบียนคำขอ

รวมรายการที่แปลงแล้วและลงทะเบียนคำขอด้วย RegistryManager:

val credentialEntries =
    mapToSdJwtEntries(sdJwtsFromStorage) + mapToMdocEntries(mdocsFromStorage)

val openidRegistryRequest = OpenId4VpRegistry(
    credentialEntries = credentialEntries,
    id = "my-wallet-openid-registry-v1" // A stable, unique ID to identify your registry record.
)

ไม่บังคับ: การออกบัตรในหน้าเว็บ

หากไม่มีข้อมูลเข้าสู่ระบบประเภทที่ขอสำหรับผู้ใช้ คุณสามารถแสดงตัวเลือกเพื่อเพิ่มข้อมูลเข้าสู่ระบบลงในที่เก็บข้อมูลได้ โดยเพิ่มรายการ inlineIssuanceEntries ที่มีข้อมูลเข้าสู่ระบบที่รองรับลงใน OpenId4VpRegistry ดังนี้

val openidRegistryRequest = OpenId4VpRegistry(
    credentialEntries = credentialEntries,
    inlineIssuanceEntries = emptyList(),
                listOf(
                MdocInlineIssuanceEntry(
                    id = "Issuance",
                    display = InlineIssuanceEntry.InlineIssuanceDisplayProperties(
                        subtitle = "Mobile Drivers License, State Id, and Others",
                    ),
                    supportedMdocs = setOf(
                        MdocInlineIssuanceEntry.SupportedMdoc(
                        "eu.europa.ec.eudi.pid.1"
                        ),
                        MdocInlineIssuanceEntry.SupportedMdoc(
                        "org.iso.18013.5.1.mDL1"
                        ),
                    )
                ),
                SdJwtInlineIssuanceEntry(
                    id = "sd-jwt-issuance",
                    display = InlineIssuanceEntry.InlineIssuanceDisplayProperties(
                        subtitle = "Mobile Drivers License, State Id, and Others",
                    ),
                    supportedSdJwts = setOf(
                        SdJwtInlineIssuanceEntry.SupportedSdJwt("urn:openid:interop:id:1")
                    )
                )
            ),
    id = "my-wallet-openid-registry-v1"
)

ลงทะเบียนข้อมูลเข้าสู่ระบบด้วย CredentialManager

try {
    val response = registryManager.registerCredentials(openidRegistryRequest)
} catch (e: Exception) {
    // Handle failure
}

การจัดการข้อมูลเมตาของแอป

ข้อมูลเมตาที่แอปผู้ถือลงทะเบียนกับ CredentialManager มีพร็อพเพอร์ตี้ต่อไปนี้

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

ไม่บังคับ: สร้างตัวจับคู่

Matcher คือไบนารี Wasm ที่ Credential Manager เรียกใช้ในแซนด์บ็อกซ์เพื่อกรองข้อมูลเข้าสู่ระบบที่ลงทะเบียนไว้กับคำขอ Verifier ขาเข้า

  • ตัวจับคู่เริ่มต้น: คลาส OpenId4VpRegistry จะรวมตัวจับคู่ OpenId4VP เริ่มต้นโดยอัตโนมัติ (OpenId4VpDefaults.DEFAULT_MATCHER) เมื่อคุณสร้างอินสแตนซ์ สำหรับกรณีการใช้งาน OpenID4VP มาตรฐานทั้งหมด ไลบรารีจะจัดการการจับคู่ให้คุณ
  • Custom matcher: คุณจะใช้ Custom matcher ก็ต่อเมื่อคุณรองรับโปรโตคอลที่ไม่ได้มาตรฐานซึ่งต้องใช้ตรรกะการจับคู่ของตัวเอง

จัดการข้อมูลเข้าสู่ระบบที่เลือก

เมื่อผู้ใช้เลือกข้อมูลเข้าสู่ระบบ แอปที่เก็บข้อมูลจะต้องจัดการคำขอ คุณจะต้องกำหนดกิจกรรมที่คอยฟังตัวกรอง Intent androidx.credentials.registry.provider.action.GET_CREDENTIAL วอลเล็ตตัวอย่างของเราแสดงให้เห็นถึงขั้นตอนนี้

Intent จะเปิดกิจกรรมของคุณด้วยคำขอ Verifier และต้นทางการโทร ซึ่งคุณดึงข้อมูลด้วย PendingIntentHandler.retrieveProviderGetCredentialRequest ฟังก์ชัน ซึ่งจะแสดงผล ProviderGetCredentialRequest ที่มีข้อมูลทั้งหมดที่เชื่อมโยงกับคำขอของผู้ตรวจสอบ ซึ่งมีองค์ประกอบหลัก 3 อย่าง

  • แอปการโทร: แอปที่ส่งคำขอ ซึ่งเรียกข้อมูลได้ด้วย getCallingAppInfo คุณสามารถใช้ข้อมูลนี้เพื่อตรวจสอบตัวตนของผู้ยืนยันได้
  • ข้อมูลเข้าสู่ระบบที่เลือก: ข้อมูลเกี่ยวกับตัวเลือกที่ผู้ใช้เลือก ซึ่งดึงมาผ่าน selectedCredentialSet extension method โดยข้อมูลนี้จะตรงกับรหัสข้อมูลเข้าสู่ระบบที่คุณลงทะเบียนไว้
  • คำขอที่เฉพาะเจาะจง: คำขอที่เฉพาะเจาะจงซึ่งส่งโดยผู้ตรวจสอบ ซึ่งดึงมาจากเมธอด getCredentialOptions สำหรับขั้นตอนคำขอข้อมูลเข้าสู่ระบบดิจิทัล คุณจะเห็น GetDigitalCredentialOption รายการเดียวในรายการนี้

คุณประมวลผลคำขอการนำเสนอได้ด้วยโค้ดต่อไปนี้

request.credentialOptions.forEach { option ->
    if (option is GetDigitalCredentialOption) {
        Log.i(TAG, "Got DC request: ${option.requestJson}")
        processRequest(option.requestJson)
    }
}

ดูตัวอย่างได้ในวอลเล็ตตัวอย่าง

ตรวจสอบข้อมูลระบุตัวตนของผู้ตรวจสอบ

ก่อนอื่น ให้ดึง ProviderGetCredentialRequest ออกจาก Intent ดังนี้

val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)

ตรวจสอบแอปที่มีสิทธิ์: แอปที่มีสิทธิ์ (เช่น เว็บเบราว์เซอร์) สามารถเรียกใช้ในนามของผู้ตรวจสอบสิทธิ์รายอื่นๆ ได้โดยการตั้งค่าพารามิเตอร์ต้นทาง หากต้องการดึงข้อมูลต้นทางนี้ ให้ส่งรายการผู้โทรที่ได้รับสิทธิ์และเชื่อถือได้ (รายการที่อนุญาตในรูปแบบ JSON) ไปยัง CallingAppInfo getOrigin API

val origin = request?.callingAppInfo?.getOrigin(
    privilegedAppsJson // Your allow list JSON
)

หาก origin ไม่ว่างเปล่า: ระบบจะแสดงผลต้นทางหาก packageName และลายนิ้วมือของใบรับรองตรงกับแอปในรายการที่อนุญาต ให้ถือว่าการเรียกนี้เป็นแบบมีสิทธิ์และตั้งค่าต้นทางนี้ในการตอบกลับ OpenID4VP แทนการคำนวณต้นทางโดยใช้ลายเซ็นของแอปที่เรียก

หาก origin ว่างเปล่า: คำขอของผู้ตรวจสอบมาจากแอป Android ต้นทางที่แมปกับการตอบกลับ OpenID4VP ควรคำนวณเป็น android:apk-key-hash:<encoded SHA 256 fingerprint>

val appSigningInfo =
    request?.callingAppInfo?.signingInfoCompat?.signingCertificateHistory?.get(0)?.toByteArray()
val md = MessageDigest.getInstance("SHA-256")
val certHash =
    Base64.encodeToString(md.digest(appSigningInfo), Base64.NO_WRAP or Base64.NO_PADDING)
val computedOrigin = "android:apk-key-hash:$certHash"

แสดงภาพ UI ของที่ยึด

เมื่อเลือกข้อมูลเข้าสู่ระบบ ระบบจะเรียกใช้แอปผู้ถือเพื่อแนะนำผู้ใช้ผ่าน UI ของแอป การจัดการเวิร์กโฟลว์นี้ทำได้ 2 วิธีมาตรฐาน ดังนี้

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

แสดงการตอบกลับข้อมูลเข้าสู่ระบบ

เมื่อแอปที่เก็บข้อมูลพร้อมที่จะส่งผลลัพธ์กลับแล้ว ให้สิ้นสุดกิจกรรมด้วยการตอบกลับข้อมูลเข้าสู่ระบบดังนี้

PendingIntentHandler.setGetCredentialResponse(
    resultData,
    GetCredentialResponse(DigitalCredential(response.responseJson))
)
setResult(RESULT_OK, resultData)
finish()

หากมีข้อยกเว้น คุณสามารถส่งข้อยกเว้นของข้อมูลเข้าสู่ระบบในลักษณะเดียวกันได้ ดังนี้

PendingIntentHandler.setGetCredentialException(
    resultData,
    GetCredentialUnknownException() // Configure the proper exception
)
setResult(RESULT_OK, resultData)
finish()

ดูตัวอย่างทั้งหมดของการแสดงผลการตอบกลับข้อมูลเข้าสู่ระบบในบริบทได้ที่แอปตัวอย่าง