Integrate single tap passkey creation and sign-in with biometric prompts

On Android 15, Credential Manager supports a single tap flow for credential creation and retrieval. In this flow, the information of the credential being created, or being used, is displayed directly in the Biometric Prompt, along with an entrypoint to more options. This simplified process creates a more efficient and streamlined credential creation and retrieval process.

Requirements:

  • Biometrics have been set up on the user's device and the user allows them for authentication into applications.
  • For sign-in flows, this feature is enabled for single account scenarios only, even if there's multiple credentials (such as passkey and password) available for that account.

Enable single tap on passkey creation flows

This method's creation steps match the existing credential creation process. Within your BeginCreatePublicKeyCredentialRequest, use handleCreatePasskeyQuery() to process the request if it is for a passkey.

is BeginCreatePublicKeyCredentialRequest -> {
  Log.i(TAG, "Request is passkey type")
  return handleCreatePasskeyQuery(request, passwordCount, passkeyCount)
}

In your handleCreatePasskeyQuery(), include BiometricPromptData with the CreateEntry class:

val createEntry = CreateEntry(
  // Additional properties...
  biometricPromptData = BiometricPromptData(
    allowedAuthenticators = allowedAuthenticator
  )
)

Credential providers should explicitly set the allowedAuthenticator property in the BiometricPromptData instance. If this property is not set, the value defaults to DEVICE_WEAK. Set the optional cryptoObject property if needed for your use case.

Enable single tap on sign-in passkey flows

Similar to the passkey creation flow, this will follow the existing setup for handling user sign-in. Under the BeginGetPublicKeyCredentialOption, use populatePasskeyData() to gather the relevant information about the authentication request:

is BeginGetPublicKeyCredentialOption -> {
  // ... other logic

  populatePasskeyData(
    origin,
    option,
    responseBuilder,
    autoSelectEnabled,
    allowedAuthenticator
  )

  // ... other logic as needed
}

Similar to CreateEntry, a BiometricPromptData instance is set to the PublicKeyCredentialEntry instance. If not explicitly set, allowedAuthenticator defaults to BIOMETRIC_WEAK.

PublicKeyCredentialEntry(
  // other properties...

  biometricPromptData = BiometricPromptData(
    allowedAuthenticators = allowedAuthenticator
  )
)

Handle credential entry selection

While handling the credential entry selection for passkey creation or passkey selection during sign in, call the PendingIntentHandler's retrieveProviderCreateCredentialRequest, or retrieveProviderGetCredentialRequest, as appropriate. These return objects that contain the metadata needed for the provider. For example, when handling passkey creation entry selection, update your code as shown:

val createRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
if (createRequest == null) {
  Log.i(TAG, "request is null")
  setUpFailureResponseAndFinish("Unable to extract request from intent")
  return
}
// Other logic...

val biometricPromptResult = createRequest.biometricPromptResult

// Add your logic based on what needs to be done
// after getting biometrics

if (createRequest.callingRequest is CreatePublicKeyCredentialRequest) {
  val publicKeyRequest: CreatePublicKeyCredentialRequest =
    createRequest.callingRequest as CreatePublicKeyCredentialRequest

  if (biometricPromptResult == null) {
    // Do your own authentication flow, if needed
  }
  else if (biometricPromptResult.isSuccessful) {
    createPasskey(
        publicKeyRequest.requestJson,
        createRequest.callingAppInfo,
        publicKeyRequest.clientDataHash,
        accountId
    )
  } else {
    val error = biometricPromptResult.authenitcationError
    // Process the error
}

  // Other logic...
}

This example contains information about the biometric flow's success. It also contains other information about the credential. If the flow fails, use the error code under biometricPromptResult.authenticationError to make decisions. The error codes returned as part of biometricPromptResult.authenticationError.errorCode are the same error codes defined in the androidx.biometric library, such as androidx.biometric.BiometricPrompt.NO_SPACE, androidx.biometric.BiometricPrompt.UNABLE_TO_PROCESS, androidx.biometric.BiometricPrompt.ERROR_TIMEOUT, and similar. The authenticationError will also contain an error message associated with the errorCode that can be displayed on a UI.

Similarly, extract metadata during the retrieveProviderGetCredentialRequest. Check if your biometric flow is null. If yes, configure your own biometrics to authenticate. This is similar to how the get operation is instrumented:

val getRequest =
    PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)

if (getRequest == null) {
  Log.i(TAG, "request is null")
  setUpFailureResponseAndFinish("Unable to extract request from intent")
  return
}

// Other logic...

val biometricPromptResult = getRequest.biometricPromptResult

// Add your logic based on what needs to be done
// after getting biometrics

if (biometricPromptResult == null)
{
  // Do your own authentication flow, if necessary
} else if (biometricPromptResult.isSuccessful) {

Log.i(TAG, "The response from the biometricPromptResult was ${biometricPromptResult.authenticationResult.authenticationType}")

validatePasskey(
    publicKeyRequest.requestJson,
    origin,
    packageName,
    uid,
    passkey.username,
    credId,
    privateKey
)
  } else {
    val error = biometricPromptResult.authenitcationError
    // Process the error
}

  // Other logic...