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

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

ตั้งค่า

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

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

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

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 ดังที่แสดงในตัวอย่างต่อไปนี้

<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 จะใช้

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

ในไดเรกทอรี res/xml ให้สร้างไฟล์ใหม่ชื่อ provider.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

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

จัดการการค้นหาสำหรับการสร้างพาสคีย์

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

  1. ลบล้างเมธอด onBeginCreateCredentialRequest() ในบริการที่ขยายจาก CredentialProviderService
  2. จัดการ BeginCreateCredentialRequest โดยสร้าง BeginCreateCredentialResponse ที่เกี่ยวข้องและส่งผ่าน Callback
  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 Extra ได้
  • PendingIntent ต้องไม่สร้างขึ้นด้วยแฟล็ก PendingIntent.FLAG_ONE_SHOT เนื่องจากผู้ใช้อาจเลือกรายการ ย้อนกลับ และเลือกรายการนั้นอีกครั้ง ซึ่งจะทำให้ PendingIntent เริ่มทำงาน 2 ครั้ง
  • PendingIntent ต้องสร้างขึ้นด้วยรหัสคำขอที่ไม่ซ้ำกันเพื่อให้แต่ละรายการมี PendingIntent ที่สอดคล้องกันของตัวเอง

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

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

ตัวอย่างโค้ดด้านล่างแสดงขั้นตอนเหล่านี้ โค้ดนี้ต้องได้รับการจัดการในคลาส Activity เมื่อมีการเรียกใช้ 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() ที่กล่าวถึงในส่วนก่อนหน้า ให้เพิ่มอีกเคสหนึ่งภายในบล็อกสวิตช์เพื่อจัดการคำขอรหัสผ่าน
  • ขณะสร้าง 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 และส่งไปยังคลาส 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 ในการตอบกลับและเรียกใช้ Callback ได้ทันที

    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 เพื่อให้ทราบว่าพาสคีย์นั้นแมปกับข้อมูลเข้าสู่ระบบใดเมื่อผู้ใช้เลือกรายการนี้

    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 Extra ซึ่งผู้ให้บริการข้อมูลเข้าสู่ระบบป้อนข้อมูลไว้เมื่อตั้งค่า 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 ในข้อมูลโค้ดด้านล่าง ระบบจะใช้คลาสข้อมูลตัวช่วย เช่น 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 credential-provider settingsActivity ใน 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 android.settings.CREDENTIAL_PROVIDER จะแสดงหน้าจอการตั้งค่าที่ผู้ใช้สามารถเลือกผู้ให้บริการข้อมูลเข้าสู่ระบบที่ต้องการและผู้ให้บริการข้อมูลเข้าสู่ระบบเพิ่มเติมได้

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

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

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

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

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

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

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

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

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

fun createSettingsPendingIntent(): PendingIntent