Mengintegrasikan Credential Manager dengan solusi penyedia kredensial

Credential Manager merujuk pada sekumpulan API yang diperkenalkan di Android 14 dan mendukung beberapa metode login seperti nama pengguna dan sandi, kunci sandi, serta solusi login federasi (seperti Login dengan Google). Saat Credential Manager API dipanggil, sistem Android akan menggabungkan kredensial dari semua penyedia kredensial yang diinstal di perangkat. Dokumen ini menjelaskan kumpulan API yang menyediakan endpoint integrasi untuk penyedia kredensial ini.

Penyiapan

Sebelum menerapkan fungsi di penyedia kredensial Anda, selesaikan langkah-langkah penyiapan yang ditampilkan di bagian berikut.

Mendeklarasikan dependensi

Dalam file build.gradle modul Anda, deklarasikan dependensi menggunakan versi terbaru library Credential Manager:

implementation "androidx.credentials:credentials:1.2.0-{latest}"

Mendeklarasikan elemen layanan dalam file manifes

Di file manifes aplikasi Anda AndroidManifest.xml, sertakan deklarasi <service> untuk class layanan yang memperluas class CredentialProviderService dari library androidx.credentials, seperti ditunjukkan dalam contoh di bawah.

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

Izin dan filter intent yang ditampilkan di atas merupakan bagian integral agar alur Credential Manager dapat berfungsi seperti yang diharapkan. Izin diperlukan sehingga hanya sistem Android yang dapat mengikat ke layanan ini. Filter intent digunakan untuk visibilitas layanan ini sebagai penyedia kredensial yang akan digunakan oleh Credential Manager.

Mendeklarasikan jenis kredensial yang didukung

Di direktori res/xml, buat file baru bernama provider.xml. Dalam file ini, deklarasikan jenis kredensial yang didukung layanan Anda melalui konstanta yang ditetapkan untuk setiap jenis kredensial di library. Pada contoh berikut, layanan mendukung sandi tradisional serta kunci sandi, yang konstantanya ditentukan sebagai TYPE_PASSWORD_CREDENTIAL dan TYPE_PUBLIC_KEY_CREDENTIAL:

<?xml version="1.0" encoding="utf-8"?>
<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>

Pada level API sebelumnya, penyedia kredensial terintegrasi dengan API seperti isi otomatis untuk sandi dan data lainnya. Penyedia ini dapat menggunakan infrastruktur internal yang sama untuk menyimpan jenis kredensial yang ada, sekaligus memperluasnya untuk mendukung yang lain, termasuk kunci sandi.

Pendekatan dua fase untuk interaksi penyedia

Credential Manager berinteraksi dengan penyedia kredensial dalam dua fase:

  1. Fase pertama adalah fase memulai/kueri yang memungkinkan sistem mengikat ke layanan penyedia kredensial dan memanggil metode onBeginGetCredentialRequest(), onBeginCreateCredentialRequest(), atau onClearCredentialStateRequest() dengan permintaan Begin…. Penyedia harus memproses permintaan ini dan merespons dengan respons Begin…, yang akan mengisinya dengan entri yang mewakili opsi visual yang akan ditampilkan di pemilih akun. Setiap entri harus memiliki kumpulan PendingIntent.
  2. Setelah pengguna memilih entri, fase pemilihan akan dimulai dan PendingIntent yang terkait dengan entri tersebut akan diaktifkan sehingga aktivitas penyedia yang sesuai akan muncul. Setelah pengguna selesai berinteraksi dengan aktivitas ini, penyedia kredensial harus menetapkan respons terhadap hasil aktivitas sebelum mengakhirinya. Respons ini kemudian dikirim ke aplikasi klien yang memanggil Credential Manager.

Menangani pembuatan kunci sandi

Menangani kueri untuk pembuatan kunci sandi

Ketika ingin membuat kunci sandi dan menyimpannya dengan penyedia kredensial, aplikasi klien akan memanggil createCredential API. Untuk menangani permintaan ini di layanan penyedia kredensial Anda agar kunci sandi benar-benar disimpan dalam penyimpanan Anda, selesaikan langkah-langkah yang ditunjukkan di bagian berikut.

  1. Ganti metode onBeginCreateCredentialRequest() di layanan Anda yang diperluas dari CredentialProviderService.
  2. Tangani BeginCreateCredentialRequest dengan membuat BeginCreateCredentialResponse yang sesuai dan meneruskannya melalui callback.
  3. Saat membuat BeginCreateCredentialResponse, tambahkan CreateEntries yang diperlukan. Setiap CreateEntry harus sesuai dengan akun tempat kredensial dapat disimpan, dan harus memiliki PendingIntent yang ditetapkan bersama metadata lain yang diperlukan.

Contoh berikut menunjukkan cara menerapkan langkah ini.

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
        )
    )
}

Konstruksi PendingIntent Anda harus mematuhi hal berikut:

  • Aktivitas yang sesuai harus disiapkan untuk menampilkan perintah, konfirmasi, atau pemilihan Biometrik yang diperlukan.
  • Setiap data yang diperlukan oleh penyedia saat aktivitas terkait dipanggil harus ditetapkan sebagai tambahan pada intent yang digunakan untuk membuat PendingIntent, seperti accountId dalam alur pembuatan.
  • PendingIntent harus dibuat dengan flag PendingIntent.FLAG_MUTABLE sehingga sistem dapat menambahkan permintaan final ke tambahan intent.
  • PendingIntent Anda tidak boleh dibuat dengan flag PendingIntent.FLAG_ONE_SHOT karena pengguna dapat memilih entri, kembali, dan memilih ulang entri, yang akan menyebabkan PendingIntent diaktifkan dua kali.
  • PendingIntent harus dibuat dengan kode permintaan unik agar setiap entri dapat memiliki PendingIntent yang sesuai.

Menangani pemilihan entri untuk permintaan pembuatan kunci sandi

  1. Saat pengguna memilih CreateEntry yang sebelumnya diisi, PendingIntent yang sesuai akan dipanggil dan Activity penyedia terkait akan dibuat.
  2. Setelah metode onCreate Aktivitas dipanggil, akses intent terkait dan teruskan ke class PendingIntentHander untuk mendapatkan ProviderCreateCredentialRequest.
  3. Ekstrak requestJson, callingAppInfo, dan clientDataHash dari permintaan.
  4. Ekstrak accountId lokal dari ekstra intent. Ini adalah implementasi khusus aplikasi contoh dan tidak diperlukan. ID akun ini dapat digunakan untuk menyimpan kredensial terhadap ID akun ini.
  5. Validasi requestJson. Contoh di bawah menggunakan class data lokal seperti PublicKeyCredentialCreationOptions untuk mengonversi JSON input menjadi class terstruktur sesuai spesifikasi WebAuthn. Sebagai penyedia kredensial, Anda dapat menggantinya dengan parser Anda sendiri.
  6. Periksa link aset untuk aplikasi panggilan jika panggilan berasal dari aplikasi Android native.
  7. Tampilkan perintah autentikasi. Contoh di bawah ini menggunakan Android Biometric API.
  8. Setelah autentikasi berhasil, buat credentialId dan pasangan kunci.
  9. Simpan kunci pribadi di database lokal Anda pada callingAppInfo.packageName.
  10. Buat respons JSON Web Authentication API yang terdiri dari kunci publik dan credentialId. Contoh di bawah ini menggunakan class utilitas lokal seperti AuthenticatorAttestationResponse dan FidoPublicKeyCredential yang membantu pembuatan JSON berdasarkan spesifikasi yang disebutkan sebelumnya. Sebagai penyedia kredensial, Anda dapat mengganti class ini dengan builder Anda sendiri.
  11. Buat CreatePublicKeyCredentialResponse dengan JSON yang dihasilkan di atas.
  12. Tetapkan CreatePublicKeyCredentialResponse sebagai tambahan di Intent melalui PendingIntentHander.setCreateCredentialResponse(), dan tetapkan intent tersebut ke hasil Aktivitas.
  13. Selesaikan Aktivitas.

Contoh kode di bawah ini menggambarkan langkah-langkah ini. Kode ini perlu ditangani di class Aktivitas Anda setelah onCreate() dipanggil.

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
  )
}

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

  val biometricPrompt = BiometricPrompt(
    this,
    <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)

        // 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
        )
        val result = Intent()

        val createPublicKeyCredResponse =
          CreatePublicKeyCredentialResponse(credential.json())

        // Set the CreateCredentialResponse as the result of the Activity
        PendingIntentHandler.setCreateCredentialResponse(
          result, createPublicKeyCredResponse
        )
        setResult(Activity.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)
}

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)}"
}

Menangani kueri untuk permintaan pembuatan sandi

Untuk menangani kueri atas permintaan pembuatan sandi, lakukan hal berikut:

  • Di dalam metode processCreateCredentialRequest() yang disebutkan di bagian sebelumnya, tambahkan kasus lain di dalam blok tombol untuk menangani permintaan sandi.
  • Saat membuat BeginCreateCredentialResponse, tambahkan CreateEntries yang diperlukan.
  • Setiap CreateEntry harus sesuai dengan akun tempat kredensial dapat disimpan, dan harus memiliki PendingIntent yang ditetapkan bersama dengan metadata lainnya.

Contoh berikut menunjukkan cara menerapkan langkah ini:

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
}

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)
}

Menangani pemilihan entri untuk permintaan pembuatan sandi

Saat pengguna memilih CreateEntry yang terisi, PendingIntent yang sesuai akan dieksekusi dan menampilkan Aktivitas terkait. Akses intent terkait yang diteruskan di onCreate dan teruskan ke class PendingIntentHander untuk mendapatkan metode ProviderCreateCredentialRequest.

Contoh di bawah menggambarkan cara menerapkan proses ini. Kode ini perlu ditangani dalam metode onCreate() Aktivitas Anda.

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

val request: CreatePasswordRequest = createRequest.callingRequest as CreatePasswordRequest

// Fetch the ID and password from the request and save it in your database
<your_database>.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)
this@<activity>.finish()

Menangani login pengguna

Login pengguna ditangani dengan langkah-langkah berikut:

  • Saat aplikasi klien mencoba membuat pengguna login, aplikasi tersebut akan menyiapkan instance GetCredentialRequest.
  • Framework Android menyebarkan permintaan ini ke semua penyedia kredensial yang berlaku dengan mengikat ke layanan tersebut.
  • Layanan penyedia kemudian menerima BeginGetCredentialRequest yang berisi daftar BeginGetCredentialOption, yang masing-masing berisi parameter yang dapat digunakan untuk mengambil kredensial yang cocok.

Untuk menangani permintaan ini di layanan penyedia kredensial Anda, selesaikan langkah-langkah berikut:

  1. Ganti metode onBeginGetCredentialRequest() untuk menangani permintaan. Perhatikan bahwa jika kredensial dikunci, Anda dapat segera menetapkan AuthenticationAction pada respons dan memanggil 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())
        }
    }
    

    Penyedia yang mewajibkan membuka kunci kredensial sebelum menampilkan credentialEntries apa pun harus menyiapkan intent tertunda yang mengarahkan pengguna ke alur buka kunci aplikasi:

    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. Ambil kredensial dari database lokal Anda dan siapkan menggunakan CredentialEntries agar ditampilkan di pemilih. Untuk kunci sandi, Anda dapat menetapkan credentialId sebagai tambahan pada intent guna mengetahui kredensial mana yang dipetakan saat pengguna memilih entri ini.

    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 processGetCredentialsRequest(
    request: BeginGetCredentialRequest
    ): BeginGetCredentialResponse {
        val callingPackage = request.callingAppInfo?.packageName
        val credentialEntries: MutableList<CredentialEntry> = mutableListOf()
    
        for (option in request.beginGetCredentialOptions) {
            when (option) {
                is BeginGetPasswordOption -> {
                    credentialEntries.addAll(
                            populatePasswordData(
                                callingPackage,
                                option
                            )
                        )
                    }
                    is BeginGetPublicKeyCredentialOption -> {
                        credentialEntries.addAll(
                            populatePasskeyData(
                                callingPackage,
                                option
                            )
                        )
                    )
                } else -> {
                    Log.i(TAG, "Request not supported")
                }
            }
        }
        return BeginGetCredentialResponse(credentialEntries)
    }
    
  3. Buat kueri kredensial dari database Anda, buat entri kunci sandi dan sandi untuk diisi.

    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
                  ),
                  beginPublicKeyCredentialOption = 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. Setelah Anda membuat kueri dan mengisi kredensial, kini Anda perlu menangani fase pemilihan untuk kredensial yang dipilih pengguna, baik dengan kunci sandi maupun sandi.

Menangani pemilihan pengguna untuk kunci sandi

  1. Dalam metode onCreate Aktivitas yang sesuai, ambil intent terkait, dan teruskan ke PendingIntentHandler.retrieveProviderGetCredentialRequest().
  2. Ekstrak GetPublicKeyCredentialOption dari permintaan yang diambil di atas. Selanjutnya, ekstrak requestJson dan clientDataHash dari opsi ini.
  3. Ekstrak credentialId dari ekstra intent yang diisi oleh penyedia kredensial saat PendingIntent yang sesuai disiapkan.
  4. Ekstrak kunci sandi dari database lokal menggunakan parameter permintaan yang diakses di atas.
  5. Nyatakan bahwa kunci sandi valid dengan metadata yang diekstrak dan verifikasi pengguna.

    val getRequest =
        PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
    val publicKeyRequest =
    getRequest.credentialOption as GetPublicKeyCredentialOption
    
    val requestInfo = intent.getBundleExtra("CREDENTIAL_DATA")
    val credIdEnc = requestInfo.getString("credId")
    
    // Get the saved passkey from your database based on the credential ID
    // from the publickeyRequest
    val passkey = <your database>.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. Untuk memvalidasi pengguna, tampilkan perintah Biometrik (atau metode pernyataan lainnya). Cuplikan kode di bawah ini menggunakan Android Biometric API.

  7. Setelah autentikasi berhasil, buat respons JSON berdasarkan spesifikasi W3 Web Authentication Assertion. Dalam cuplikan kode di bawah ini, class data helper seperti AuthenticatorAssertionResponse digunakan untuk mengambil parameter terstruktur dan mengonversinya menjadi format JSON yang diperlukan. Respons berisi tanda tangan digital dari kunci pribadi kredensial WebAuthn. Server pihak tepercaya dapat memverifikasi tanda tangan ini untuk mengautentikasi pengguna sebelum login.

  8. Buat PublicKeyCredential menggunakan JSON yang dihasilkan di atas lalu tetapkan pada GetCredentialResponse akhir. Tetapkan respons akhir ini pada hasil aktivitas ini.

Contoh berikut menggambarkan cara menerapkan langkah-langkah ini:

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

val biometricPrompt = BiometricPrompt(
    this,
    <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
        )
        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)

Menangani pemilihan pengguna untuk autentikasi sandi

  1. Di aktivitas yang sesuai, akses intent yang diteruskan ke onCreate dan ekstrak ProviderGetCredentialRequest menggunakan PendingIntentHandler.
  2. Gunakan GetPasswordOption dalam permintaan untuk mengambil kredensial sandi untuk nama paket yang masuk.

    val getRequest =
    PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
    
    val passwordOption = getRequest.credentialOption as GetPasswordCredentialOption
    
    val username = passwordOption.username
    // Fetch the credentials for the calling app package name
    val creds = <your_database>.getCredentials(callingAppInfo.packageName)
    val passwords = creds.passwords
    val it = passwords.iterator()
    var password = ""
    while (it.hasNext() == true) {
        val passwordItemCurrent = it.next()
        if (passwordItemCurrent.username == username) {
           password = passwordItemCurrent.password
           break
        }
    }
    
  3. Setelah diambil, tetapkan respons untuk kredensial sandi yang dipilih.

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

Menangani pemilihan entri tindakan autentikasi

Seperti disebutkan sebelumnya, penyedia kredensial dapat menetapkan AuthenticationAction jika kredensial dikunci. Jika pengguna memilih entri ini, Aktivitas yang sesuai dengan tindakan intent yang ditetapkan di PendingIntent akan dipanggil. Penyedia kredensial kemudian dapat menampilkan alur autentikasi biometrik atau mekanisme serupa untuk membuka kredensial. Jika berhasil, penyedia kredensial harus membuat BeginGetCredentialResponse, mirip dengan cara penanganan login pengguna dijelaskan di atas, karena kredensial sekarang terbuka. Respons ini kemudian harus ditetapkan melalui metode PendingIntentHandler.setBeginGetCredentialResponse() sebelum intent yang disiapkan ditetapkan sebagai hasilnya dan Aktivitas selesai.

Menghapus permintaan kredensial

Aplikasi klien dapat meminta agar setiap status yang dipertahankan untuk pemilihan kredensial harus dihapus, seperti penyedia kredensial dapat mengingat kredensial yang dipilih sebelumnya dan hanya menampilkannya di lain waktu. Aplikasi klien memanggil API ini dan mengharapkan pemilihan melekat tersebut akan dihapus. Layanan penyedia kredensial Anda dapat menangani permintaan ini dengan mengganti metode onClearCredentialStateRequest():

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

Agar pengguna dapat membuka setelan penyedia dari layar Sandi, kunci sandi, & isi otomatis, aplikasi penyedia kredensial harus menerapkan atribut manifes credential-provider settingsActivity di res/xml/provider.xml. Atribut ini memungkinkan Anda menggunakan intent untuk membuka layar setelan aplikasi Anda sendiri jika pengguna mengklik nama penyedia dalam daftar layanan Sandi, kunci sandi, & isi otomatis. Tetapkan nilai atribut ini ke nama aktivitas yang akan diluncurkan dari layar setelan.

<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>
Diagram yang menunjukkan fungsi tombol ubah dan buka
Gambar 1: Tombol Ubah akan membuka dialog pemilihan yang ada, sehingga pengguna dapat memilih penyedia kredensial yang diinginkan. Tombol Buka meluncurkan aktivitas setelan yang ditentukan dalam perubahan manifes, dan membuka halaman setelan khusus untuk penyedia tersebut.

Intent setelan

Buka setelan: Intent android.settings.CREDENTIAL_PROVIDER akan menampilkan layar setelan tempat pengguna dapat memilih penyedia kredensial tambahan dan pilihan mereka.

Layar setelan Sandi, kunci sandi, dan isi otomatis
Gambar 2: Layar Sandi, kunci sandi, dan setelan isi otomatis.

Layanan kredensial pilihan: Intent ACTION_REQUEST_SET_AUTOFILL_SERVICE mengalihkan pengguna ke layar pemilihan penyedia pilihan. Penyedia yang dipilih di layar ini menjadi penyedia isi otomatis dan kredensial pilihan.

Diagram yang menunjukkan fungsi tombol buka dan berubah
Gambar 3: Layanan pilihan untuk sandi, kunci sandi, dan layar setelan isi otomatis.

Mendapatkan daftar aplikasi dengan hak istimewa yang diizinkan

Aplikasi dengan hak istimewa seperti browser web melakukan panggilan Credential Manager atas nama pihak tepercaya lainnya dengan menetapkan parameter origin di Credential Manager GetCredentialRequest() dan metode CreatePublicKeyCredentialRequest(). Untuk memproses permintaan ini, penyedia kredensial mengambil origin menggunakan getOrigin() API.

Untuk mengambil origin, aplikasi penyedia kredensial harus meneruskan daftar pemanggil dengan hak istimewa dan tepercaya ke androidx.credentials.provider.CallingAppInfo's getOrigin() API. Daftar yang diizinkan ini harus berupa objek JSON yang valid. origin akan ditampilkan jika packageName dan sidik jari sertifikat yang didapatkan dari signingInfo cocok dengan aplikasi yang ditemukan di privilegedAllowlist yang diteruskan ke getOrigin() API. Setelah nilai origin didapatkan, aplikasi penyedia harus menganggapnya sebagai panggilan dengan hak istimewa dan menyetel origin ini pada data klien di AuthenticatorResponse, bukan menghitung origin menggunakan tanda tangan aplikasi panggilan.

Jika Anda mengambil origin, gunakan clientDataHash yang disediakan langsung di CreatePublicKeyCredentialRequest() atau GetPublicKeyCredentialOption(), bukan menyusun dan melakukan hashing clientDataJSON selama permintaan tanda tangan. Untuk menghindari masalah penguraian JSON, tetapkan nilai placeholder untuk clientDataJSON dalam respons pengesahan dan pernyataan. Pengelola Sandi Google menggunakan daftar yang diizinkan yang tersedia secara terbuka untuk panggilan ke getOrigin(). Sebagai penyedia kredensial, Anda dapat menggunakan daftar ini atau memberikan daftar Anda sendiri dalam format JSON yang dijelaskan oleh API. Penyedia dapat memilih daftar mana yang akan digunakan. Untuk mendapatkan akses hak istimewa dengan penyedia kredensial pihak ketiga, lihat dokumentasi yang disediakan oleh pihak ketiga.

Mengaktifkan penyedia di perangkat

Pengguna harus mengaktifkan penyedia melalui device settings > Passwords & Accounts > Your Provider > Enable or Disable.

fun createSettingsPendingIntent(): PendingIntent