ผสานรวมเครื่องมือจัดการข้อมูลเข้าสู่ระบบกับโซลูชันของผู้ให้บริการข้อมูลเข้าสู่ระบบ

เครื่องมือจัดการข้อมูลเข้าสู่ระบบหมายถึงชุด API ที่เปิดตัวใน Android 14 ซึ่งรองรับวิธีการลงชื่อเข้าใช้หลายวิธี เช่น ชื่อผู้ใช้และรหัสผ่าน พาสคีย์ และโซลูชันการลงชื่อเข้าใช้แบบรวมศูนย์ (เช่น การลงชื่อเข้าใช้ด้วย Google) เมื่อมีการเรียกใช้ Credential Manager API ระบบ Android จะรวบรวมข้อมูลเข้าสู่ระบบจากผู้ให้บริการข้อมูลเข้าสู่ระบบทั้งหมดที่ติดตั้งในอุปกรณ์ เอกสารนี้อธิบายชุด API ที่มีปลายทางการผสานรวมสำหรับผู้ให้บริการข้อมูลเข้าสู่ระบบเหล่านี้

ตั้งค่า

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

ประกาศทรัพยากร Dependency

เพิ่มทรัพยากร Dependency ต่อไปนี้ลงในสคริปต์บิลด์ของโมดูลแอปเพื่อใช้ Credential Manager Library เวอร์ชันล่าสุด

Kotlin

dependencies {
    implementation("androidx.credentials:credentials:1.7.0-alpha02")
}

Groovy

dependencies {
    implementation "androidx.credentials:credentials:1.7.0-alpha02"
}

ประกาศองค์ประกอบบริการในไฟล์ Manifest

ในไฟล์ Manifest ของแอป AndroidManifest.xml ให้ใส่การประกาศ <service> สำหรับคลาสบริการที่ขยายคลาส CredentialProviderService จาก androidx.credentials Library ดังที่ แสดงในตัวอย่างต่อไปนี้

<service android:name=".MyCredentialProviderService"
    android:enabled="true"
    android:exported="true"
    android:label="My Credential Provider"
    android:icon="@mipmap/ic_launcher"
    android:permission="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE"
    tools:targetApi="upside_down_cake">
    <intent-filter>
        <action android:name="android.service.credentials.CredentialProviderService"/>
    </intent-filter>
    <meta-data
        android:name="android.credentials.provider"
        android:resource="@xml/provider"/>
</service>

สิทธิ์และตัวกรอง Intent ที่แสดงในตัวอย่างก่อนหน้านี้เป็นส่วนสำคัญเพื่อให้โฟลว์ของ Credential Manager ทำงานได้ตามที่คาดไว้ ต้องมีสิทธิ์เพื่อให้ระบบ Android เท่านั้นที่เชื่อมโยงกับบริการนี้ได้ ตัวกรอง Intent ใช้สำหรับการค้นพบบริการนี้ในฐานะผู้ให้บริการข้อมูลเข้าสู่ระบบที่ Credential Manager จะใช้

ประกาศประเภทข้อมูลเข้าสู่ระบบที่รองรับ

สร้างไฟล์ใหม่ชื่อ provider.xml ในไดเรกทอรี res/xml ในไฟล์นี้ ให้ประกาศประเภทข้อมูลเข้าสู่ระบบที่บริการของคุณรองรับผ่านค่าคงที่ที่กำหนดไว้สำหรับข้อมูลเข้าสู่ระบบแต่ละประเภทในไลบรารี ในตัวอย่างต่อไปนี้ บริการรองรับรหัสผ่านแบบดั้งเดิมและพาสคีย์ โดยค่าคงที่สำหรับรหัสผ่านและพาสคีย์กำหนดไว้เป็น TYPE_PASSWORD_CREDENTIAL และ TYPE_PUBLIC_KEY_CREDENTIAL

<credential-provider xmlns:android="http://schemas.android.com/apk/res/android">
    <capabilities>
        <capability name="android.credentials.TYPE_PASSWORD_CREDENTIAL" />
        <capability name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" />
    </capabilities>
</credential-provider>

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

แนวทาง 2 ระยะสำหรับการโต้ตอบกับผู้ให้บริการ

Credential Manager โต้ตอบกับผู้ให้บริการข้อมูลเข้าสู่ระบบใน 2 ระยะ ดังนี้

  1. ระยะแรกคือ ระยะเริ่มต้น/ระยะการค้นหา ซึ่งระบบจะเชื่อมโยงกับ บริการของผู้ให้บริการข้อมูลเข้าสู่ระบบและเรียกใช้ onBeginGetCredentialRequest(), onBeginCreateCredentialRequest() หรือ onClearCredentialStateRequest() เมธอดด้วย Begin… คำขอ ผู้ให้บริการต้องประมวลผลคำขอเหล่านี้และตอบกลับด้วยการตอบกลับ Begin… โดยกรอกข้อมูลด้วยรายการที่แสดงตัวเลือกแบบภาพที่จะแสดงในตัวเลือกบัญชี แต่ละรายการต้องมีการตั้งค่า PendingIntent
  2. เมื่อผู้ใช้เลือกรายการแล้ว ระยะการเลือก จะเริ่มต้นขึ้นและ PendingIntent ที่เชื่อมโยงกับรายการจะเริ่มทำงาน ซึ่งจะแสดง กิจกรรมของผู้ให้บริการที่เกี่ยวข้อง เมื่อผู้ใช้โต้ตอบกับกิจกรรมนี้เสร็จแล้ว ผู้ให้บริการข้อมูลเข้าสู่ระบบต้องตั้งค่าการตอบกลับเป็นผลลัพธ์ของกิจกรรมก่อนที่จะสิ้นสุดกิจกรรม จากนั้นระบบจะส่งการตอบกลับนี้ไปยังแอปของไคลเอ็นต์ที่เรียกใช้ Credential Manager

จัดการการสร้างพาสคีย์

จัดการการค้นหาเพื่อสร้างพาสคีย์

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

  1. ลบล้างเมธอด onBeginCreateCredentialRequest() ในบริการที่ ขยายจาก CredentialProviderService
  2. จัดการ BeginCreateCredentialRequest โดยสร้าง BeginCreateCredentialResponse ที่เกี่ยวข้องและส่งผ่าน การเรียกกลับ
  3. ขณะสร้าง BeginCreateCredentialResponse ให้เพิ่ม ที่จำเป็น CreateEntries CreateEntry แต่ละรายการควรสอดคล้องกับ บัญชีที่บันทึกข้อมูลเข้าสู่ระบบได้ และต้องมีการตั้งค่า PendingIntent พร้อมกับข้อมูลเมตาอื่นๆ ที่จำเป็น

ตัวอย่างต่อไปนี้แสดงวิธีใช้ขั้นตอนเหล่านี้

override fun onBeginCreateCredentialRequest(
    request: BeginCreateCredentialRequest,
    cancellationSignal: CancellationSignal,
    callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException>,
) {
    val response: BeginCreateCredentialResponse? = processCreateCredentialRequest(request)
    if (response != null) {
        callback.onResult(response)
    } else {
        callback.onError(CreateCredentialUnknownException())
    }
}

fun processCreateCredentialRequest(request: BeginCreateCredentialRequest): BeginCreateCredentialResponse? {
    when (request) {
        is BeginCreatePublicKeyCredentialRequest -> {
            // Request is passkey type
            return handleCreatePasskeyQuery(request)
        }
    }
    // Request not supported
    return null
}

private fun handleCreatePasskeyQuery(
    request: BeginCreatePublicKeyCredentialRequest
): BeginCreateCredentialResponse {

    // Adding two create entries - one for storing credentials to the 'Personal'
    // account, and one for storing them to the 'Family' account. These
    // accounts are local to this sample app only.
    val createEntries: MutableList<CreateEntry> = mutableListOf()
    createEntries.add(
        CreateEntry(
            PERSONAL_ACCOUNT_ID,
            createNewPendingIntent(PERSONAL_ACCOUNT_ID, CREATE_PASSKEY_INTENT)
        )
    )

    createEntries.add(
        CreateEntry(
            FAMILY_ACCOUNT_ID,
            createNewPendingIntent(FAMILY_ACCOUNT_ID, CREATE_PASSKEY_INTENT)
        )
    )

    return BeginCreateCredentialResponse(createEntries)
}

private fun createNewPendingIntent(accountId: String, action: String): PendingIntent {
    val intent = Intent(action).setPackage(PACKAGE_NAME)

    // Add your local account ID as an extra to the intent, so that when
    // user selects this entry, the credential can be saved to this
    // account
    intent.putExtra(EXTRA_KEY_ACCOUNT_ID, accountId)

    return PendingIntent.getActivity(
        applicationContext, UNIQUE_REQ_CODE,
        intent,
        (
            PendingIntent.FLAG_MUTABLE
                or PendingIntent.FLAG_UPDATE_CURRENT
            )
    )
}

การสร้าง PendingIntent ควรเป็นไปตามข้อกำหนดต่อไปนี้

  • ควรตั้งค่ากิจกรรมที่เกี่ยวข้องให้แสดงพรอมต์ไบโอเมตริก การยืนยัน หรือการเลือกที่จำเป็น
  • ควรตั้งค่าข้อมูลที่จำเป็นซึ่งผู้ให้บริการต้องการเมื่อมีการเรียกใช้กิจกรรมที่เกี่ยวข้องเป็นข้อมูลเพิ่มเติมใน Intent ที่ใช้สร้าง PendingIntent เช่น accountId ในโฟลว์การสร้าง
  • ต้องสร้าง PendingIntent ด้วยแฟล็ก PendingIntent.FLAG_MUTABLE เพื่อให้ระบบสามารถเพิ่มคำขอสุดท้าย ลงในข้อมูลเพิ่มเติมของ Intent ได้
  • ต้องไม่สร้าง PendingIntent ด้วยแฟล็ก PendingIntent.FLAG_ONE_SHOT เนื่องจากผู้ใช้อาจเลือกรายการ กลับไป และ เลือกรายการอีกครั้ง ซึ่งจะทำให้ PendingIntent เริ่มทำงาน 2 ครั้ง
  • ต้องสร้าง PendingIntent ด้วยรหัสคำขอที่ไม่ซ้ำกันเพื่อให้แต่ละรายการมี PendingIntent ที่เกี่ยวข้องของตัวเองได้

จัดการการเลือกรายการสำหรับคำขอสร้างพาสคีย์

  1. เมื่อผู้ใช้เลือก CreateEntry ที่ป้อนข้อมูลไว้ก่อนหน้านี้ ระบบจะเรียกใช้ PendingIntent ที่เกี่ยวข้องและสร้าง Activity ของผู้ให้บริการที่เชื่อมโยง
  2. หลังจากเรียกใช้เมธอด onCreate ของกิจกรรมแล้ว ให้เข้าถึง Intent ที่เชื่อมโยงและส่ง Intent นั้นไปยังคลาส PendingIntentHander เพื่อรับ ProviderCreateCredentialRequest
  3. แยก requestJson, callingAppInfo และ clientDataHash ออกจากคำขอ
  4. แยก accountId ในเครื่องออกจากข้อมูลเพิ่มเติมของ Intent นี่คือการติดตั้งใช้งานเฉพาะแอปตัวอย่างและไม่จำเป็น คุณสามารถใช้รหัสบัญชีนี้เพื่อจัดเก็บข้อมูลเข้าสู่ระบบกับรหัสบัญชีนี้ได้
  5. ตรวจสอบ requestJson ตัวอย่างด้านล่างใช้คลาสข้อมูลในเครื่อง เช่น PublicKeyCredentialCreationOptions เพื่อแปลง JSON อินพุตเป็นคลาสที่มีโครงสร้างตามข้อกำหนด WebAuthn ในฐานะผู้ให้บริการข้อมูลเข้าสู่ระบบ คุณสามารถแทนที่คลาสนี้ด้วยตัวแยกวิเคราะห์ของคุณเอง
  6. ตรวจสอบ Asset Link สำหรับแอปการโทรหากการเรียกใช้มาจาก แอป Android ดั้งเดิม
  7. แสดงพรอมต์การตรวจสอบสิทธิ์ ตัวอย่างด้านล่างใช้ Android Biometric API
  8. เมื่อการตรวจสอบสิทธิ์สำเร็จ ให้สร้าง credentialId และ key pair
  9. บันทึกคีย์ส่วนตัวในฐานข้อมูลในเครื่องกับ callingAppInfo.packageName
  10. สร้างการตอบกลับ JSON ของ Web Authentication API ที่ ประกอบด้วยคีย์สาธารณะ และ credentialId ตัวอย่างด้านล่างใช้คลาสยูทิลิตีในเครื่อง เช่น AuthenticatorAttestationResponse และ FidoPublicKeyCredential ซึ่งช่วยสร้าง JSON ตามข้อกำหนดที่กล่าวถึงก่อนหน้านี้ ในฐานะผู้ให้บริการข้อมูลเข้าสู่ระบบ คุณสามารถแทนที่คลาสเหล่านี้ด้วยตัวสร้างของคุณเอง
  11. สร้าง CreatePublicKeyCredentialResponse ด้วย JSON ที่สร้างขึ้นข้างต้น
  12. ตั้งค่า CreatePublicKeyCredentialResponse เป็นข้อมูลเพิ่มเติมใน Intent ผ่าน PendingIntentHander.setCreateCredentialResponse() และตั้งค่า Intent นั้นเป็นผลลัพธ์ของกิจกรรม
  13. สิ้นสุดกิจกรรม

ตัวอย่างโค้ดด้านล่างแสดงขั้นตอนเหล่านี้ คุณต้องจัดการโค้ดนี้ในคลาสกิจกรรมเมื่อมีการเรียกใช้ onCreate()

override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
    super.onCreate(savedInstanceState, persistentState)
    // ...

    val request =
        PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)

    val accountId = intent.getStringExtra(CredentialsRepo.EXTRA_KEY_ACCOUNT_ID)
    if (request != null && request.callingRequest is CreatePublicKeyCredentialRequest) {
        val publicKeyRequest: CreatePublicKeyCredentialRequest =
            request.callingRequest as CreatePublicKeyCredentialRequest
        createPasskey(
            publicKeyRequest.requestJson,
            request.callingAppInfo,
            publicKeyRequest.clientDataHash,
            accountId
        )
    }
}

@SuppressLint("RestrictedApi")
fun createPasskey(
    requestJson: String,
    callingAppInfo: CallingAppInfo?,
    clientDataHash: ByteArray?,
    accountId: String?
) {
    val request = PublicKeyCredentialCreationOptions(requestJson)

    val biometricPrompt = BiometricPrompt(
        this,
        { }, // Pass in your own executor
        object : AuthenticationCallback() {
            override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                super.onAuthenticationError(errorCode, errString)
                finish()
            }

            override fun onAuthenticationFailed() {
                super.onAuthenticationFailed()
                finish()
            }

            @RequiresApi(VERSION_CODES.P)
            override fun onAuthenticationSucceeded(
                result: AuthenticationResult
            ) {
                super.onAuthenticationSucceeded(result)

                // Generate a credentialId
                val credentialId = ByteArray(32)
                SecureRandom().nextBytes(credentialId)

                // Generate a credential key pair
                val spec = ECGenParameterSpec("secp256r1")
                val keyPairGen = KeyPairGenerator.getInstance("EC")
                keyPairGen.initialize(spec)
                val keyPair = keyPairGen.genKeyPair()

                // Save passkey in your database as per your own implementation

                // Create AuthenticatorAttestationResponse object to pass to
                // FidoPublicKeyCredential

                val response = AuthenticatorAttestationResponse(
                    requestOptions = request,
                    credentialId = credentialId,
                    credentialPublicKey = getPublicKeyFromKeyPair(keyPair),
                    origin = appInfoToOrigin(callingAppInfo!!),
                    up = true,
                    uv = true,
                    be = true,
                    bs = true,
                    packageName = callingAppInfo.packageName
                )

                val credential = FidoPublicKeyCredential(
                    rawId = credentialId,
                    response = response,
                    authenticatorAttachment = "", // Add your authenticator attachment
                )
                val result = Intent()

                val createPublicKeyCredResponse =
                    CreatePublicKeyCredentialResponse(credential.json())

                // Set the CreateCredentialResponse as the result of the Activity
                PendingIntentHandler.setCreateCredentialResponse(
                    result,
                    createPublicKeyCredResponse
                )
                setResult(RESULT_OK, result)
                finish()
            }
        }
    )

    val promptInfo = BiometricPrompt.PromptInfo.Builder()
        .setTitle("Use your screen lock")
        .setSubtitle("Create passkey for ${request.rp.name}")
        .setAllowedAuthenticators(
            BiometricManager.Authenticators.BIOMETRIC_STRONG
            /* or BiometricManager.Authenticators.DEVICE_CREDENTIAL */
        )
        .build()
    biometricPrompt.authenticate(promptInfo)
}

@RequiresApi(VERSION_CODES.P)
fun appInfoToOrigin(info: CallingAppInfo): String {
    val cert = info.signingInfo.apkContentsSigners[0].toByteArray()
    val md = MessageDigest.getInstance("SHA-256")
    val certHash = md.digest(cert)
    // This is the format for origin
    return "android:apk-key-hash:${b64Encode(certHash)}"
}

จัดการการค้นหาสำหรับคำขอสร้างรหัสผ่าน

หากต้องการจัดการการค้นหาสำหรับคำขอสร้างรหัสผ่าน ให้ทำดังนี้

  • ภายในเมธอด processCreateCredentialRequest() ที่กล่าวถึงในส่วนก่อนหน้า ให้เพิ่มอีกกรณีหนึ่งภายในบล็อก Switch เพื่อจัดการคำขอรหัสผ่าน
  • ขณะสร้าง BeginCreateCredentialResponse ให้เพิ่ม CreateEntries ที่จำเป็น
  • CreateEntry แต่ละรายการควรสอดคล้องกับบัญชีที่บันทึกข้อมูลเข้าสู่ระบบได้ และต้องมีการตั้งค่า PendingIntent พร้อมกับข้อมูลเมตาอื่นๆ

ตัวอย่างต่อไปนี้แสดงวิธีใช้ขั้นตอนเหล่านี้

fun processCreateCredentialRequest(
    request: BeginCreateCredentialRequest
): BeginCreateCredentialResponse? {
    when (request) {
        is BeginCreatePublicKeyCredentialRequest -> {
            // Request is passkey type
            return handleCreatePasskeyQuery(request)
        }

        is BeginCreatePasswordCredentialRequest -> {
            // Request is password type
            return handleCreatePasswordQuery(request)
        }
    }
    return null
}

@RequiresApi(VERSION_CODES.M)
private fun handleCreatePasswordQuery(
    request: BeginCreatePasswordCredentialRequest
): BeginCreateCredentialResponse {
    val createEntries: MutableList<CreateEntry> = mutableListOf()

    // Adding two create entries - one for storing credentials to the 'Personal'
    // account, and one for storing them to the 'Family' account. These
    // accounts are local to this sample app only.
    createEntries.add(
        CreateEntry(
            PERSONAL_ACCOUNT_ID,
            createNewPendingIntent(PERSONAL_ACCOUNT_ID, CREATE_PASSWORD_INTENT)
        )
    )
    createEntries.add(
        CreateEntry(
            FAMILY_ACCOUNT_ID,
            createNewPendingIntent(FAMILY_ACCOUNT_ID, CREATE_PASSWORD_INTENT)
        )
    )

    return BeginCreateCredentialResponse(createEntries)
}

จัดการการเลือกรายการสำหรับคำขอสร้างรหัสผ่าน

เมื่อผู้ใช้เลือก CreateEntry ที่ป้อนข้อมูลไว้ PendingIntent ที่เกี่ยวข้องจะทำงานและแสดงกิจกรรมที่เชื่อมโยง เข้าถึง Intent ที่เชื่อมโยงซึ่งส่งผ่านใน onCreate และส่ง Intent นั้นไปยังคลาส PendingIntentHander เพื่อรับเมธอด ProviderCreateCredentialRequest

ตัวอย่างด้านล่างแสดงวิธีใช้กระบวนการนี้ คุณต้องจัดการโค้ดนี้ในเมธอด onCreate() ของกิจกรรม

val createRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
val accountId = intent.getStringExtra(CredentialsRepo.EXTRA_KEY_ACCOUNT_ID)

if (createRequest == null) {
    return
}

val request: CreatePasswordRequest = createRequest.callingRequest as CreatePasswordRequest

// Fetch the ID and password from the request and save it in your database
mDatabase.addNewPassword(
    PasswordInfo(
        request.id,
        request.password,
        createRequest.callingAppInfo.packageName
    )
)

// Set the final response back
val result = Intent()
val response = CreatePasswordResponse()
PendingIntentHandler.setCreateCredentialResponse(result, response)
setResult(Activity.RESULT_OK, result)
finish()

จัดการการลงชื่อเข้าใช้ของผู้ใช้

ระบบจะจัดการการลงชื่อเข้าใช้ของผู้ใช้ด้วยขั้นตอนต่อไปนี้

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

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

  1. ลบล้างเมธอด onBeginGetCredentialRequest() เพื่อจัดการคำขอ โปรดทราบว่าหากข้อมูลเข้าสู่ระบบล็อกอยู่ คุณสามารถตั้งค่า AuthenticationAction ในการตอบกลับและเรียกใช้การเรียกกลับได้ทันที

    private val unlockEntryTitle = "Authenticate to continue"
    
    override fun onBeginGetCredentialRequest(
        request: BeginGetCredentialRequest,
        cancellationSignal: CancellationSignal,
        callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,
    ) {
        if (isAppLocked()) {
            callback.onResult(
                BeginGetCredentialResponse(
                    authenticationActions = mutableListOf(
                        AuthenticationAction(
                            unlockEntryTitle, createUnlockPendingIntent()
                        )
                    )
                )
            )
            return
        }
        try {
            response = processGetCredentialRequest(request)
            callback.onResult(response)
        } catch (e: GetCredentialException) {
            callback.onError(GetCredentialUnknownException())
        }
    }
    

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

    private fun createUnlockPendingIntent(): PendingIntent {
        val intent = Intent(UNLOCK_INTENT).setPackage(PACKAGE_NAME)
        return PendingIntent.getActivity(
            applicationContext, UNIQUE_REQUEST_CODE, intent,
            (
                PendingIntent.FLAG_MUTABLE
                    or PendingIntent.FLAG_UPDATE_CURRENT
                )
        )
    }
    
  2. ดึงข้อมูลเข้าสู่ระบบจากฐานข้อมูลในเครื่องและตั้งค่าโดยใช้ CredentialEntries เพื่อแสดงในตัวเลือก สำหรับพาสคีย์ คุณสามารถตั้งค่า credentialId เป็นข้อมูลเพิ่มเติมใน Intent เพื่อให้ทราบว่าข้อมูลเข้าสู่ระบบใดที่ Intent นั้นเชื่อมโยงอยู่เมื่อผู้ใช้เลือกรายการนี้

    companion object {
        // These intent actions are specified for corresponding activities
        // that are to be invoked through the PendingIntent(s)
        private const val GET_PASSKEY_INTENT_ACTION = "PACKAGE_NAME.GET_PASSKEY"
        private const val GET_PASSWORD_INTENT_ACTION = "PACKAGE_NAME.GET_PASSWORD"
    }
    
    fun processGetCredentialRequest(
        request: BeginGetCredentialRequest
    ): BeginGetCredentialResponse {
        val callingPackageInfo = request.callingAppInfo
        val callingPackageName = callingPackageInfo?.packageName.orEmpty()
        val credentialEntries: MutableList<CredentialEntry> = mutableListOf()
    
        for (option in request.beginGetCredentialOptions) {
            when (option) {
                is BeginGetPasswordOption -> {
                    credentialEntries.addAll(
                        populatePasswordData(
                            callingPackageName,
                            option
                        )
                    )
                }
                is BeginGetPublicKeyCredentialOption -> {
                    credentialEntries.addAll(
                        populatePasskeyData(
                            callingPackageInfo,
                            option
                        )
                    )
                } else -> {
                    Log.i(TAG, "Request not supported")
                }
            }
        }
        return BeginGetCredentialResponse(credentialEntries)
    }
    
  3. ค้นหาข้อมูลเข้าสู่ระบบจากฐานข้อมูล สร้างรายการพาสคีย์และรหัสผ่านเพื่อป้อนข้อมูล

    private fun populatePasskeyData(
        callingAppInfo: CallingAppInfo?,
        option: BeginGetPublicKeyCredentialOption
    ): List<CredentialEntry> {
        val passkeyEntries: MutableList<CredentialEntry> = mutableListOf()
        val request = PublicKeyCredentialRequestOptions(option.requestJson)
        // Get your credentials from database where you saved during creation flow
        val creds = getCredentialsFromInternalDb(request.rpId)
        val passkeys = creds.passkeys
        for (passkey in passkeys) {
            val data = Bundle()
            data.putString("credId", passkey.credId)
            passkeyEntries.add(
                PublicKeyCredentialEntry(
                    context = applicationContext,
                    username = passkey.username,
                    pendingIntent = createNewPendingIntent(
                        GET_PASSKEY_INTENT_ACTION,
                        data
                    ),
                    beginGetPublicKeyCredentialOption = option,
                    displayName = passkey.displayName,
                    icon = passkey.icon
                )
            )
        }
        return passkeyEntries
    }
    
    // Fetch password credentials and create password entries to populate to the user
    private fun populatePasswordData(
        callingPackage: String,
        option: BeginGetPasswordOption
    ): List<CredentialEntry> {
        val passwordEntries: MutableList<CredentialEntry> = mutableListOf()
    
        // Get your password credentials from database where you saved during
        // creation flow
        val creds = getCredentialsFromInternalDb(callingPackage)
        val passwords = creds.passwords
        for (password in passwords) {
            passwordEntries.add(
                PasswordCredentialEntry(
                    context = applicationContext,
                    username = password.username,
                    pendingIntent = createNewPendingIntent(
                        GET_PASSWORD_INTENT
                    ),
                    beginGetPasswordOption = option,
                    displayName = password.username,
                    icon = password.icon
                )
            )
        }
        return passwordEntries
    }
    
    private fun createNewPendingIntent(
        action: String,
        extra: Bundle? = null
    ): PendingIntent {
        val intent = Intent(action).setPackage(PACKAGE_NAME)
        if (extra != null) {
            intent.putExtra("CREDENTIAL_DATA", extra)
        }
    
        return PendingIntent.getActivity(
            applicationContext, UNIQUE_REQUEST_CODE, intent,
            (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
        )
    }
    
  4. เมื่อค้นหาและป้อนข้อมูลเข้าสู่ระบบแล้ว ตอนนี้คุณต้องจัดการระยะการเลือกสำหรับข้อมูลเข้าสู่ระบบที่ผู้ใช้เลือก ไม่ว่าจะเป็นพาสคีย์หรือรหัสผ่าน

การจัดการการเลือกของผู้ใช้สำหรับพาสคีย์

  1. ในเมธอด onCreate ของกิจกรรมที่เกี่ยวข้อง ให้ดึง Intent ที่เชื่อมโยงและส่งไปยัง PendingIntentHandler.retrieveProviderGetCredentialRequest()
  2. แยก GetPublicKeyCredentialOption ออกจากคำขอที่ดึงข้อมูล ข้างต้น จากนั้นแยก requestJson และ clientDataHash ออกจากตัวเลือกนี้
  3. แยก credentialId ออกจากข้อมูลเพิ่มเติมของ Intent ซึ่งผู้ให้บริการข้อมูลเข้าสู่ระบบป้อนข้อมูลไว้เมื่อมีการตั้งค่า PendingIntent ที่เกี่ยวข้อง
  4. แยกพาสคีย์ออกจากฐานข้อมูลในเครื่องโดยใช้พารามิเตอร์คำขอที่เข้าถึงข้างต้น
  5. ยืนยันว่าพาสคีย์ถูกต้องด้วยข้อมูลเมตาที่แยกออกมาและการยืนยันตัวตนของผู้ใช้

    val getRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
    val publicKeyRequest = getRequest?.credentialOptions?.first() as GetPublicKeyCredentialOption
    
    val requestInfo = intent.getBundleExtra("CREDENTIAL_DATA")
    val credIdEnc = requestInfo?.getString("credId").orEmpty()
    
    // Get the saved passkey from your database based on the credential ID from the PublicKeyRequest
    val passkey = mDatabase.getPasskey(credIdEnc)
    
    // Decode the credential ID, private key and user ID
    val credId = b64Decode(credIdEnc)
    val privateKey = b64Decode(passkey.credPrivateKey)
    val uid = b64Decode(passkey.uid)
    
    val origin = appInfoToOrigin(getRequest.callingAppInfo)
    val packageName = getRequest.callingAppInfo.packageName
    
    validatePasskey(
        publicKeyRequest.requestJson,
        origin,
        packageName,
        uid,
        passkey.username,
        credId,
        privateKey
    )
    
  6. หากต้องการยืนยันตัวตนของผู้ใช้ ให้แสดงพรอมต์ไบโอเมตริก (หรือวิธีการยืนยันอื่นๆ) ข้อมูลโค้ดด้านล่างใช้ Android Biometric API

  7. เมื่อการตรวจสอบสิทธิ์สำเร็จ ให้สร้างการตอบกลับ JSON ตามข้อกำหนดการยืนยันตัวตนของ W3 Web ในข้อมูลโค้ด ด้านล่าง มีการใช้คลาสข้อมูลตัวช่วย เช่น AuthenticatorAssertionResponse เพื่อ รับพารามิเตอร์ที่มีโครงสร้างและแปลงพารามิเตอร์เหล่านั้นเป็นรูปแบบ JSON ที่จำเป็น การตอบกลับจะมีลายเซ็นดิจิทัลจาก คีย์ส่วนตัวของข้อมูลเข้าสู่ระบบ WebAuthn เซิร์ฟเวอร์ของผู้ให้บริการสามารถยืนยันลายเซ็นนี้เพื่อตรวจสอบสิทธิ์ผู้ใช้ก่อนลงชื่อเข้าใช้

  8. สร้าง PublicKeyCredential โดยใช้ JSON ที่สร้างขึ้นข้างต้นและ ตั้งค่าใน GetCredentialResponse สุดท้าย ตั้งค่าการตอบกลับสุดท้ายนี้เป็นผลลัพธ์ของกิจกรรมนี้

ตัวอย่างต่อไปนี้แสดงวิธีใช้ขั้นตอนเหล่านี้

val request = PublicKeyCredentialRequestOptions(requestJson)
val privateKey: ECPrivateKey = convertPrivateKey(privateKeyBytes)

val biometricPrompt = BiometricPrompt(
    this,
    { }, // Pass in your own executor
    object : BiometricPrompt.AuthenticationCallback() {
        override fun onAuthenticationError(
            errorCode: Int,
            errString: CharSequence
        ) {
            super.onAuthenticationError(errorCode, errString)
            finish()
        }

        override fun onAuthenticationFailed() {
            super.onAuthenticationFailed()
            finish()
        }

        override fun onAuthenticationSucceeded(
            result: BiometricPrompt.AuthenticationResult
        ) {
            super.onAuthenticationSucceeded(result)
            val response = AuthenticatorAssertionResponse(
                requestOptions = request,
                credentialId = credId,
                origin = origin,
                up = true,
                uv = true,
                be = true,
                bs = true,
                userHandle = uid,
                packageName = packageName
            )

            val sig = Signature.getInstance("SHA256withECDSA")
            sig.initSign(privateKey)
            sig.update(response.dataToSign())
            response.signature = sig.sign()

            val credential = FidoPublicKeyCredential(
                rawId = credId,
                response = response,
                authenticatorAttachment = "", // Add your authenticator attachment
            )
            val result = Intent()
            val passkeyCredential = PublicKeyCredential(credential.json())
            PendingIntentHandler.setGetCredentialResponse(
                result, GetCredentialResponse(passkeyCredential)
            )
            setResult(RESULT_OK, result)
            finish()
        }
    }
)

val promptInfo = BiometricPrompt.PromptInfo.Builder()
    .setTitle("Use your screen lock")
    .setSubtitle("Use passkey for ${request.rpId}")
    .setAllowedAuthenticators(
        BiometricManager.Authenticators.BIOMETRIC_STRONG
        /* or BiometricManager.Authenticators.DEVICE_CREDENTIAL */
    )
    .build()
biometricPrompt.authenticate(promptInfo)

การจัดการการเลือกของผู้ใช้สำหรับการตรวจสอบสิทธิ์ด้วยรหัสผ่าน

  1. ในกิจกรรมที่เกี่ยวข้อง ให้เข้าถึง Intent ที่ส่งผ่านไปยัง onCreate และแยก ProviderGetCredentialRequest โดยใช้ PendingIntentHandler
  2. ใช้ GetPasswordOption ในคำขอเพื่อดึงข้อมูลเข้าสู่ระบบรหัสผ่าน สำหรับชื่อแพ็กเกจขาเข้า

    val getRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
    
    val passwordOption = getRequest?.credentialOptions?.first() as GetPasswordOption
    
    val username = passwordOption.allowedUserIds.first()
    // Fetch the credentials for the calling app package name
    val creds = mDatabase.getCredentials(callingAppInfo.packageName)
    val passwords = creds.passwords
    val it = passwords.iterator()
    var password = ""
    while (it.hasNext()) {
        val passwordItemCurrent = it.next()
        if (passwordItemCurrent.username == username) {
            password = passwordItemCurrent.password
            break
        }
    }
    
  3. เมื่อดึงข้อมูลเข้าสู่ระบบรหัสผ่านที่เลือกแล้ว ให้ตั้งค่าการตอบกลับสำหรับข้อมูลเข้าสู่ระบบรหัสผ่านนั้น

    // Set the response back
    val result = Intent()
    val passwordCredential = PasswordCredential(username, password)
    PendingIntentHandler.setGetCredentialResponse(
        result, GetCredentialResponse(passwordCredential)
    )
    setResult(Activity.RESULT_OK, result)
    finish()
    

จัดการการเลือกรายการการดำเนินการตรวจสอบสิทธิ์

ดังที่ กล่าวไว้ก่อนหน้านี้ ผู้ให้บริการข้อมูลเข้าสู่ระบบสามารถตั้งค่า AuthenticationAction ได้หากข้อมูลเข้าสู่ระบบล็อกอยู่ หากผู้ใช้เลือกรายการนี้ ระบบจะเรียกใช้กิจกรรมที่สอดคล้องกับการดำเนินการผ่าน Intent ที่ตั้งค่าไว้ใน PendingIntent จากนั้นผู้ให้บริการข้อมูลเข้าสู่ระบบสามารถแสดงโฟลว์การตรวจสอบสิทธิ์ด้วยไบโอเมตริกหรือกลไกที่คล้ายกันเพื่อปลดล็อกข้อมูลเข้าสู่ระบบ เมื่อสำเร็จแล้ว ผู้ให้บริการข้อมูลเข้าสู่ระบบต้องสร้าง BeginGetCredentialResponse ในลักษณะเดียวกับที่อธิบายไว้ข้างต้นสำหรับการจัดการการลงชื่อเข้าใช้ของผู้ใช้ เนื่องจากตอนนี้ข้อมูลเข้าสู่ระบบ ปลดล็อกแล้ว จากนั้นต้องตั้งค่าการตอบกลับนี้ผ่านเมธอด PendingIntentHandler.setBeginGetCredentialResponse() ก่อนที่จะตั้งค่า Intent ที่ เตรียมไว้เป็นผลลัพธ์และสิ้นสุดกิจกรรม

ล้างคำขอข้อมูลเข้าสู่ระบบ

แอปของไคลเอ็นต์อาจขอให้ล้างสถานะที่เก็บไว้สำหรับการเลือกข้อมูลเข้าสู่ระบบ เช่น ผู้ให้บริการข้อมูลเข้าสู่ระบบอาจจดจำข้อมูลเข้าสู่ระบบที่เลือกไว้ก่อนหน้านี้และแสดงผลเฉพาะข้อมูลเข้าสู่ระบบนั้นในครั้งถัดไป แอปของไคลเอ็นต์จะเรียกใช้ API นี้และคาดหวังให้ล้างการเลือกแบบคงอยู่ บริการของผู้ให้บริการข้อมูลเข้าสู่ระบบ สามารถจัดการคำขอนี้ได้โดยการลบล้างเมธอด onClearCredentialStateRequest()

override fun onClearCredentialStateRequest(
    request: ProviderClearCredentialStateRequest,
    cancellationSignal: CancellationSignal,
    callback: OutcomeReceiver<Void?, ClearCredentialException>
) {
    // Delete any maintained state as appropriate.
}

หากต้องการอนุญาตให้ผู้ใช้เปิดการตั้งค่าของผู้ให้บริการจากหน้าจอรหัสผ่าน พาสคีย์ และการป้อนข้อความอัตโนมัติ แอปของผู้ให้บริการข้อมูลเข้าสู่ระบบควรใช้แอตทริบิวต์ Manifest settingsActivity ของ credential-provider ใน res/xml/provider.xml แอตทริบิวต์นี้ช่วยให้คุณใช้ Intent เพื่อเปิดหน้าจอการตั้งค่าของแอปเองได้หากผู้ใช้คลิกชื่อผู้ให้บริการในรายการบริการรหัสผ่าน พาสคีย์ และการป้อนข้อความอัตโนมัติ ตั้งค่าแอตทริบิวต์นี้เป็นชื่อของกิจกรรมที่จะเปิดจากหน้าจอการตั้งค่า

<credential-provider
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:settingsSubtitle="Example settings provider name"
    android:settingsActivity="com.example.SettingsActivity">
    <capabilities>
        <capability name="android.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" />
    </capabilities>
</credential-provider>
แผนภาพแสดงฟังก์ชันของปุ่มเปลี่ยนและปุ่มเปิด
รูปที่ 1: ปุ่มเปลี่ยน จะเปิดกล่องโต้ตอบการเลือกที่มีอยู่ ซึ่งช่วยให้ผู้ใช้เลือกผู้ให้บริการข้อมูลเข้าสู่ระบบที่ต้องการได้ ปุ่มเปิด จะเปิดใช้กิจกรรมการตั้งค่าที่กำหนดไว้ในการเปลี่ยนแปลง Manifest และเปิดหน้าการตั้งค่าสำหรับผู้ให้บริการรายนั้นโดยเฉพาะ

Intent การตั้งค่า

เปิดการตั้งค่า: Intent android.settings.CREDENTIAL_PROVIDER จะแสดงหน้าจอการตั้งค่าที่ผู้ใช้สามารถเลือกผู้ให้บริการข้อมูลเข้าสู่ระบบที่ต้องการและ ผู้ให้บริการข้อมูลเข้าสู่ระบบเพิ่มเติมได้

หน้าจอการตั้งค่ารหัสผ่าน พาสคีย์ และการป้อนข้อความอัตโนมัติ
รูปที่ 2: หน้าจอการตั้งค่ารหัสผ่าน พาสคีย์ และการป้อนข้อความอัตโนมัติ

บริการข้อมูลเข้าสู่ระบบที่ต้องการ: Intent ACTION_REQUEST_SET_AUTOFILL_SERVICE จะเปลี่ยนเส้นทางผู้ใช้ไปยัง หน้าจอการเลือกผู้ให้บริการที่ต้องการ ผู้ให้บริการที่เลือกในหน้าจอนี้จะกลายเป็นผู้ให้บริการข้อมูลเข้าสู่ระบบและการป้อนข้อความอัตโนมัติที่ต้องการ

แผนภาพแสดงฟังก์ชันของปุ่มเปลี่ยนและปุ่มเปิด
รูปที่ 3: หน้าจอการตั้งค่าบริการที่ต้องการสำหรับรหัสผ่าน พาสคีย์ และการป้อนข้อความอัตโนมัติ

รับรายการที่อนุญาตของแอปที่มีสิทธิ์

แอปที่มีสิทธิ์ เช่น เว็บเบราว์เซอร์ จะเรียกใช้ Credential Manager ในนามของผู้ให้บริการรายอื่นโดยการตั้งค่าพารามิเตอร์ origin ในเมธอด GetCredentialRequest() และ CreatePublicKeyCredentialRequest() ของ Credential Manager หากต้องการประมวลผลคำขอเหล่านี้ ผู้ให้บริการข้อมูลเข้าสู่ระบบจะดึงข้อมูล origin โดยใช้ getOrigin() API

หากต้องการดึงข้อมูล origin แอปของผู้ให้บริการข้อมูลเข้าสู่ระบบต้องส่งรายการผู้เรียกที่มีสิทธิ์และเชื่อถือได้ไปยัง androidx.credentials.provider.CallingAppInfo's getOrigin() API รายการที่อนุญาตนี้ต้องเป็นออบเจ็กต์ JSON ที่ถูกต้อง ระบบจะแสดงผล origin หาก packageName และลายนิ้วมือของใบรับรองที่ได้รับจาก signingInfo ตรงกับลายนิ้วมือของใบรับรองของแอปที่พบใน privilegedAllowlist ซึ่งส่งไปยัง getOrigin() API หลังจากได้รับค่า origin แล้ว แอปผู้ให้บริการควรพิจารณาว่าการเรียกนี้เป็นการเรียกที่ได้รับสิทธิ์ และตั้งค่า origin นี้ในข้อมูลของไคลเอ็นต์ ใน AuthenticatorResponse แทนที่จะคำนวณ origin โดยใช้ลายเซ็นของแอปที่เรียกใช้

หากดึงข้อมูล origin ให้ใช้ clientDataHash ที่ระบุไว้โดยตรง ใน CreatePublicKeyCredentialRequest() หรือ GetPublicKeyCredentialOption() แทนที่จะประกอบและแฮช clientDataJSON ระหว่างคำขอการยืนยัน ตั้งค่าค่าตัวยึดตำแหน่งสำหรับ clientDataJSON ในการตอบกลับเอกสารรับรองและการยืนยันเพื่อหลีกเลี่ยงปัญหาการแยกวิเคราะห์ JSON เครื่องมือจัดการรหัสผ่านบน Google ใช้ รายการที่อนุญาตแบบเปิดสำหรับการเรียกใช้ getOrigin() ในฐานะผู้ให้บริการข้อมูลเข้าสู่ระบบ คุณสามารถใช้รายการนี้หรือระบุรายการของคุณเองในรูปแบบ JSON ที่ API อธิบายไว้ ผู้ให้บริการเป็นผู้เลือกรายการที่จะใช้ หากต้องการรับสิทธิ์เข้าถึงที่มีสิทธิ์กับผู้ให้บริการข้อมูลเข้าสู่ระบบบุคคลที่สาม โปรดดูเอกสารประกอบที่ผู้ให้บริการบุคคลที่สามจัดทำขึ้น

เปิดใช้ผู้ให้บริการในอุปกรณ์

ผู้ใช้ต้องเปิดใช้ผู้ให้บริการผ่านการตั้งค่าอุปกรณ์ > รหัสผ่านและบัญชี > ผู้ให้บริการของคุณ > เปิดใช้หรือปิดใช้

fun createSettingsPendingIntent(): PendingIntent