Skip to content

Commit

Permalink
Create BiometricErrorUtils to handle biometric validation method used…
Browse files Browse the repository at this point in the history
… in 4 classes and updated strings as suggested
  • Loading branch information
Niharika Arora committed Feb 28, 2025
1 parent 6a043d3 commit ea78f1e
Show file tree
Hide file tree
Showing 8 changed files with 140 additions and 201 deletions.
2 changes: 1 addition & 1 deletion CredentialProvider/MyVault/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The app demonstrates how to:
- Register as a `CredentialProviderService` so that users can store and retrieve passwords and passkeys using the app.
- Save passwords/passkeys to the app. These are stored locally in a database for demonstration purposes only. In a real app this data should be sent to a server to allow the user's credentials to be synchronized across all their devices.
- Retrieve credentials from the app to assist with user login in another app or website.
- Implement your own biometrics prompt(single tap credential creation & sign-in)
- Implement your own biometric prompts for single-tap credential creation and sign-in.
- Delete passkeys or passwords.

# Requirements
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.authentication.myvault

import android.annotation.SuppressLint
import android.content.Context
import androidx.credentials.provider.BiometricPromptResult

/**
* Utility class for handling biometric authentication errors.
*
* <p>This class provides a collection of static utility methods for managing
* and processing errors that may occur during biometric authentication flows.
* It encapsulates the logic for extracting error information from
* {@link BiometricPromptResult} objects and constructing user-friendly error
* messages.
*
* <p>The primary function of this class is to centralize the error-handling
* logic related to biometric authentication, promoting code reuse and
* maintainability.
*/
object BiometricErrorUtils {

/**
* Checks if there was an error during the biometric authentication flow and returns an error message if so.
*
* <p>This method determines whether the biometric authentication flow resulted in
* an error. It checks if the {@link BiometricPromptResult} is null or if the
* authentication was successful. If neither of these conditions is met, it
* extracts the error code and message from the {@link BiometricPromptResult},
* constructs an error message, and returns it.
*
* <p>The error message is built using the following format:
* "Biometric Error Code: [errorCode] [errorMessage] Other providers may be available."
*
* @param context The context used to retrieve string resources.
* @param biometricPromptResult The result of the biometric authentication prompt.
* @return An error message if there was an error during the biometric flow, or an empty string otherwise.
*/
@SuppressLint("StringFormatMatches")
fun getBiometricErrorMessage(
context: Context,
biometricPromptResult: BiometricPromptResult?,
): String {
// If the biometricPromptResult is null, there was no error.
if (biometricPromptResult == null) return context.getString(R.string.empty)

// If the biometricPromptResult indicates success, there was no error.
if (biometricPromptResult.isSuccessful) return context.getString(R.string.empty)

// Initialize default values for the error code and message.
var biometricAuthErrorCode = -1
var biometricAuthErrorMsg = context.getString(R.string.unknown_failure)

// Check if there is an authentication error in the biometricPromptResult.
if (biometricPromptResult.authenticationError != null) {
// Extract the error code and message from the authentication error.
biometricAuthErrorCode = biometricPromptResult.authenticationError!!.errorCode
biometricAuthErrorMsg = biometricPromptResult.authenticationError!!.errorMsg.toString()
}

// Build the error message to be sent to the client.
val errorMessage = buildString {
append(
context.getString(
R.string.biometric_error_code_with_message,
biometricAuthErrorCode,
),
)
append(biometricAuthErrorMsg)
append(context.getString(R.string.other_providers_error_message))
}

// Indicate that there was an error during the biometric flow.
return errorMessage
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -186,26 +186,26 @@ class CredentialsRepository(
/**
* Configures a {@link PasswordCredentialEntry.Builder} for a given password item.
*
* @param passwordItemCurrent The {@link PasswordItem} containing the password details.
* @param currentPasswordItem The {@link PasswordItem} containing the password details.
* @param option The {@link BeginGetPasswordOption} containing the request parameters.
* @return A {@link PasswordCredentialEntry.Builder} configured with the provided
* password details and request options.
*/
private fun configurePasswordCredentialEntryBuilder(
passwordItemCurrent: PasswordItem,
currentPasswordItem: PasswordItem,
option: BeginGetPasswordOption,
): PasswordCredentialEntry.Builder {
val entryBuilder = PasswordCredentialEntry.Builder(
applicationContext,
passwordItemCurrent.username,
currentPasswordItem.username,
createNewPendingIntent(
passwordItemCurrent.username,
currentPasswordItem.username,
GET_PASSWORD_INTENT,
),
option,
).setDisplayName("display-${passwordItemCurrent.username}")
).setDisplayName("display-${currentPasswordItem.username}")
.setIcon(AppDependencies.providerIcon!!)
.setLastUsedTime(Instant.ofEpochMilli(passwordItemCurrent.lastUsedTimeMs))
.setLastUsedTime(Instant.ofEpochMilli(currentPasswordItem.lastUsedTimeMs))
return entryBuilder
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
*/
package com.example.android.authentication.myvault.ui

import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.content.pm.SigningInfo
Expand All @@ -29,12 +28,12 @@ import androidx.biometric.BiometricPrompt.PromptInfo.Builder
import androidx.credentials.CreatePublicKeyCredentialRequest
import androidx.credentials.CreatePublicKeyCredentialResponse
import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.BiometricPromptResult
import androidx.credentials.provider.CallingAppInfo
import androidx.credentials.provider.PendingIntentHandler
import androidx.credentials.provider.ProviderCreateCredentialRequest
import androidx.fragment.app.FragmentActivity
import com.example.android.authentication.myvault.AppDependencies
import com.example.android.authentication.myvault.BiometricErrorUtils
import com.example.android.authentication.myvault.R
import com.example.android.authentication.myvault.data.PasskeyMetadata
import com.example.android.authentication.myvault.fido.AssetLinkVerifier
Expand Down Expand Up @@ -102,11 +101,18 @@ class CreatePasskeyActivity : FragmentActivity() {
private fun handleCreatePublicKeyCredentialRequest(request: ProviderCreateCredentialRequest) {
val accountId = intent.getStringExtra(KEY_ACCOUNT_ID)

// Retrieve the biometric prompt result from the request.
// Retrieve the BiometricPromptResult from the request.
val biometricPromptResult = request.biometricPromptResult

// Check if there was an error during the biometric flow. If so, handle the error and return.
if (isValidBiometricFlowError(biometricPromptResult)) return
// Validate the error message in biometric result
val biometricErrorMessage =
BiometricErrorUtils.getBiometricErrorMessage(this, biometricPromptResult)

// If there's valid biometric error, set up the failure response and finish.
if (biometricErrorMessage.isNotEmpty()) {
setUpFailureResponseAndFinish(biometricErrorMessage)
return
}

// access the associated intent and pass it into the PendingIntentHandler class to get the ProviderCreateCredentialRequest.
if (request.callingRequest is CreatePublicKeyCredentialRequest) {
Expand Down Expand Up @@ -138,50 +144,6 @@ class CreatePasskeyActivity : FragmentActivity() {
}
}

/**
* Checks if there was an error during the biometric authentication flow.
*
* This method determines whether the biometric authentication flow resulted in
* an error. It checks if the {@link BiometricPromptResult} is null or if the
* authentication was successful. If neither of these conditions is met, it
* extracts the error code and message from the {@link BiometricPromptResult}
* and sets up a failure response to be sent to the client.
*
* @param biometricPromptResult The result of the biometric authentication prompt.
* @return True if there was an error during the biometric flow, false otherwise.
*/
@SuppressLint("StringFormatMatches")
private fun isValidBiometricFlowError(biometricPromptResult: BiometricPromptResult?): Boolean {
// If the biometricPromptResult is null, there was no error.
if (biometricPromptResult == null) return false
if (biometricPromptResult.isSuccessful) return false

// Initialize default values for the error code and message.
var biometricAuthErrorCode = -1
var biometricAuthErrorMsg = getString(R.string.unknown_failure)

// Check if there is an authentication error in the biometricPromptResult.
if (biometricPromptResult.authenticationError != null) {
biometricAuthErrorCode = biometricPromptResult.authenticationError!!.errorCode
biometricAuthErrorMsg = biometricPromptResult.authenticationError!!.errorMsg.toString()
}

// Build the error message to be sent to the client.
setUpFailureResponseAndFinish(
buildString {
append(
getString(
R.string.biometric_error_code_with_message,
biometricAuthErrorCode,
),
)
append(biometricAuthErrorMsg)
append(getString(R.string.other_providers_error_message))
},
)
return true
}

/**
* This method validates the digital asset linking to verify the app identity,
* surface biometric prompt and sends back response to client app for passkey created
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
*/
package com.example.android.authentication.myvault.ui

import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
Expand All @@ -24,11 +23,11 @@ import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.rememberCoroutineScope
import androidx.credentials.CreatePasswordRequest
import androidx.credentials.CreatePasswordResponse
import androidx.credentials.provider.BiometricPromptResult
import androidx.credentials.provider.PendingIntentHandler
import androidx.credentials.provider.ProviderCreateCredentialRequest
import androidx.lifecycle.lifecycleScope
import com.example.android.authentication.myvault.AppDependencies
import com.example.android.authentication.myvault.BiometricErrorUtils
import com.example.android.authentication.myvault.data.PasswordMetaData
import com.example.android.authentication.myvault.ui.password.PasswordScreen
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -61,11 +60,17 @@ class CreatePasswordActivity : ComponentActivity() {
accountId: String?,
) {
if (createRequest != null) {
// Retrieve the biometric prompt result from the createRequest.
// Retrieve the BiometricPromptResult from the request.
val biometricPromptResult = createRequest.biometricPromptResult

// Check if there was an error during the biometric flow. If so, handle the error and return.
if (isValidBiometricFlowError(biometricPromptResult)) return
// Validate the error message in biometric result
val biometricErrorMessage =
BiometricErrorUtils.getBiometricErrorMessage(this, biometricPromptResult)

// If there's valid biometric error, set up the failure response and finish.
if (biometricErrorMessage.isNotEmpty()) {
return
}

if (createRequest.callingRequest is CreatePasswordRequest) {
val request: CreatePasswordRequest =
Expand Down Expand Up @@ -148,32 +153,6 @@ class CreatePasswordActivity : ComponentActivity() {
}
}

/**
* Checks if there was an error during the biometric authentication flow.
*
* This method determines whether the biometric authentication flow resulted in
* an error. It checks if the {@link BiometricPromptResult} is null or if the
* authentication was successful. If neither of these conditions is met, it
* returns true, indicating an error. Otherwise, it returns false.
*
* Note: This method does not handle the error itself. It only determines
* if an error occurred. The error handling is expected to be done elsewhere.
*
* @param biometricPromptResult The result of the biometric authentication prompt.
* @return True if there was an error during the biometric flow, false otherwise.
*/
@SuppressLint("StringFormatMatches")
private fun isValidBiometricFlowError(biometricPromptResult: BiometricPromptResult?): Boolean {
// If the biometricPromptResult is null, there was no error.
if (biometricPromptResult == null) return false

// If the biometricPromptResult indicates success, there was no error.
if (biometricPromptResult.isSuccessful) return false

// If we reach this point, there was an error during the biometric flow.
return true
}

/**
* Saves the password and sets the response back to the calling app.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
*/
package com.example.android.authentication.myvault.ui

import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.content.pm.SigningInfo
Expand All @@ -37,6 +36,7 @@ import androidx.credentials.provider.PendingIntentHandler
import androidx.credentials.provider.ProviderGetCredentialRequest
import androidx.fragment.app.FragmentActivity
import com.example.android.authentication.myvault.AppDependencies
import com.example.android.authentication.myvault.BiometricErrorUtils
import com.example.android.authentication.myvault.R
import com.example.android.authentication.myvault.data.PasskeyItem
import com.example.android.authentication.myvault.fido.AssetLinkVerifier
Expand Down Expand Up @@ -118,8 +118,15 @@ class GetPasskeyActivity : FragmentActivity() {
// Retrieve the BiometricPromptResult from the request.
val biometricPromptResult = request.biometricPromptResult

// Check if there was an error in the biometric flow.
if (isValidBiometricFlowError(biometricPromptResult)) return
// Validate the error message in biometric result
val biometricErrorMessage =
BiometricErrorUtils.getBiometricErrorMessage(this, biometricPromptResult)

// If there's valid biometric error, set up the failure response and finish.
if (biometricErrorMessage.isNotEmpty()) {
setUpFailureResponseAndFinish(biometricErrorMessage)
return
}

// Configure the passkey assertion.
configurePasskeyAssertion(
Expand Down Expand Up @@ -222,60 +229,6 @@ class GetPasskeyActivity : FragmentActivity() {
}
}

/**
* Checks if there was an error during the biometric authentication flow.
*
* <p>This method determines whether the biometric authentication flow resulted in
* an error. It checks if the {@link BiometricPromptResult} is null or if the
* authentication was successful. If neither of these conditions is met, it
* extracts the error code and message from the {@link BiometricPromptResult},
* constructs an error message, and sets up a failure response to be sent to
* the client.
*
* <p>The error message is built using the following format:
* "Biometric Error Code: [errorCode] [errorMessage] Other providers may be available."
*
* @param biometricPromptResult The result of the biometric authentication prompt.
* @return True if there was an error during the biometric flow, false otherwise.
*/
@SuppressLint("StringFormatMatches")
private fun isValidBiometricFlowError(biometricPromptResult: BiometricPromptResult?): Boolean {
// If the biometricPromptResult is null, there was no error.
if (biometricPromptResult == null) return false

// If the biometricPromptResult indicates success, there was no error.
if (biometricPromptResult.isSuccessful) return false

// Initialize default values for the error code and message.
var biometricAuthErrorCode = -1
var biometricAuthErrorMsg = getString(R.string.unknown_failure)

// Check if there is an authentication error in the biometricPromptResult.
if (biometricPromptResult.authenticationError != null) {
// Extract the error code and message from the authentication error.
biometricAuthErrorCode = biometricPromptResult.authenticationError!!.errorCode
biometricAuthErrorMsg = biometricPromptResult.authenticationError!!.errorMsg.toString()
}

// Build the error message to be sent to the client.
val errorMessage = buildString {
append(
getString(
R.string.biometric_error_code_with_message,
biometricAuthErrorCode,
),
)
append(biometricAuthErrorMsg)
append(getString(R.string.other_providers_error_message))
}

// Set up the failure response and finish the flow with the constructed error message.
setUpFailureResponseAndFinish(errorMessage)

// Indicate that there was an error during the biometric flow.
return true
}

/**
* This method helps check the asset linking to verify client app idenity
* @param rpId : Relying party identifier
Expand Down
Loading

0 comments on commit ea78f1e

Please sign in to comment.