From 0f2ac48a7bc0f0322d890793e5c5e91c5ce77ab1 Mon Sep 17 00:00:00 2001 From: Sai Venkat Desu Date: Fri, 31 May 2024 18:59:57 +0530 Subject: [PATCH 01/30] feat: added initial changes for biometrics support via BiometricManager Signed-off-by: Sai Venkat Desu --- auth0/build.gradle | 3 + .../biometrics/BiometricAuthenticators.kt | 9 + .../storage/SecureCredentialsManager.kt | 166 +++++++++++------- 3 files changed, 113 insertions(+), 65 deletions(-) create mode 100644 auth0/src/main/java/com/auth0/android/authentication/biometrics/BiometricAuthenticators.kt diff --git a/auth0/build.gradle b/auth0/build.gradle index 6b1f8c02..cafd4066 100644 --- a/auth0/build.gradle +++ b/auth0/build.gradle @@ -91,6 +91,9 @@ dependencies { implementation 'com.google.code.gson:gson:2.8.9' implementation 'com.google.androidbrowserhelper:androidbrowserhelper:2.4.0' + def biometricLibraryVersion = "1.1.0" + compileOnly "androidx.biometric:biometric:$biometricLibraryVersion" + testImplementation 'junit:junit:4.13.2' testImplementation 'org.hamcrest:java-hamcrest:2.0.0.0' testImplementation "org.powermock:powermock-module-junit4:$powermockVersion" diff --git a/auth0/src/main/java/com/auth0/android/authentication/biometrics/BiometricAuthenticators.kt b/auth0/src/main/java/com/auth0/android/authentication/biometrics/BiometricAuthenticators.kt new file mode 100644 index 00000000..b9246a7b --- /dev/null +++ b/auth0/src/main/java/com/auth0/android/authentication/biometrics/BiometricAuthenticators.kt @@ -0,0 +1,9 @@ +package com.auth0.android.authentication.biometrics + + +import android.hardware.biometrics.BiometricManager.Authenticators +public enum class BiometricAuthenticators private constructor (public val value: Int) { + STRONG (Authenticators.BIOMETRIC_STRONG), + WEAK (Authenticators.BIOMETRIC_WEAK), + DEVICE_CREDENTIAL (Authenticators.DEVICE_CREDENTIAL); +} \ No newline at end of file diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt index de4a1703..87c5b1f8 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt @@ -1,23 +1,21 @@ package com.auth0.android.authentication.storage import android.app.Activity -import android.app.KeyguardManager import android.content.Context -import android.content.Intent -import android.os.Build +import android.hardware.biometrics.BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE +import android.hardware.biometrics.BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED +import android.hardware.biometrics.BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE +import android.hardware.biometrics.BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED import android.text.TextUtils import android.util.Base64 import android.util.Log -import androidx.activity.ComponentActivity -import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.contract.ActivityResultContracts +import androidx.biometric.BiometricPrompt import androidx.annotation.IntRange import androidx.annotation.VisibleForTesting -import androidx.lifecycle.Lifecycle +import androidx.biometric.BiometricManager +import androidx.fragment.app.FragmentActivity import com.auth0.android.Auth0Exception import com.auth0.android.authentication.AuthenticationAPIClient -import com.auth0.android.authentication.AuthenticationException -import com.auth0.android.callback.AuthenticationCallback import com.auth0.android.callback.Callback import com.auth0.android.request.internal.GsonProvider import com.auth0.android.result.Credentials @@ -29,6 +27,7 @@ import java.util.concurrent.Executor import java.util.concurrent.Executors import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException +import com.auth0.android.authentication.biometrics.BiometricAuthenticators /** * A safer alternative to the [CredentialsManager] class. A combination of RSA and AES keys is used to keep the values secure. @@ -45,17 +44,42 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT //Changeable by the user private var authenticateBeforeDecrypt: Boolean - private var authenticationRequestCode = -1 - private var activity: Activity? = null - private var activityResultContract: ActivityResultLauncher? = null + private var biometricPromptInfo: BiometricPrompt.PromptInfo? = null + private var biometricPrompt: BiometricPrompt? = null //State for retrying operations private var decryptCallback: Callback? = null - private var authIntent: Intent? = null private var scope: String? = null private var minTtl = 0 private var forceRefresh = false + + private val authenticationCallback: BiometricPrompt.AuthenticationCallback = + object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + decryptCallback?.let { + continueGetCredentials( + scope, minTtl, emptyMap(), emptyMap(), forceRefresh, + decryptCallback!! + ) + } + } + + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + decryptCallback!!.onFailure(CredentialsManagerException("Biometrics Authentication Failed with error code ${errorCode} due to ${errString}")) + decryptCallback = null + + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + decryptCallback!!.onFailure(CredentialsManagerException("The user didn't pass the authentication challenge.")) + decryptCallback = null + } + } + /** * Creates a new SecureCredentialsManager to handle Credentials * @@ -97,63 +121,69 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT title: String?, description: String? ): Boolean { - require(!(requestCode < 1 || requestCode > 255)) { "Request code must be a value between 1 and 255." } - val kManager = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager - authIntent = kManager.createConfirmDeviceCredentialIntent(title, description) - authenticateBeforeDecrypt = - ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && kManager.isDeviceSecure || kManager.isKeyguardSecure) - && authIntent != null) - if (authenticateBeforeDecrypt) { - authenticationRequestCode = requestCode - - /* - * https://developer.android.com/training/basics/intents/result#register - * Docs say it's safe to call "registerForActivityResult" BEFORE the activity is created. In practice, - * when that's not the case, a RuntimeException is thrown. The lifecycle state check below is meant to - * prevent that exception while still falling back to the old "startActivityForResult" flow. That's in - * case devs are invoking this method in places other than the Activity's "OnCreate" method. - */ - if (activity is ComponentActivity && !activity.lifecycle.currentState.isAtLeast( - Lifecycle.State.STARTED - ) - ) { - activityResultContract = - activity.registerForActivityResult( - ActivityResultContracts.StartActivityForResult(), - activity.activityResultRegistry - ) { - checkAuthenticationResult(authenticationRequestCode, it.resultCode) - } - } else { - this.activity = activity - } + if (activity is FragmentActivity) { + return requireAuthentication(activity = activity, title = "Biometric Authentication") + } else { + Log.e(TAG, "requireAuthentication() needs an activity of type FragmentActivity to support Biometrics Authentication") + return false } - return authenticateBeforeDecrypt } - /** - * Checks the result after showing the LockScreen to the user. - * Must be called from the [Activity.onActivityResult] method with the received parameters. - * Called internally when your activity is a subclass of ComponentActivity (using Activity Results API). - * It's safe to call this method even if [SecureCredentialsManager.requireAuthentication] was unsuccessful. - * - * @param requestCode the request code received in the onActivityResult call. - * @param resultCode the result code received in the onActivityResult call. - * @return true if the result was handled, false otherwise. - */ - public fun checkAuthenticationResult(requestCode: Int, resultCode: Int): Boolean { - if (requestCode != authenticationRequestCode || decryptCallback == null) { + + // i feel its better we return back an enum so that they can act on top of it, rather than skimming through logs + // for eg: if they tried for strong, but user has not enrolled then they can direct them to enroll or something like that + + public fun requireAuthentication( + activity: FragmentActivity, + title: String, + subtitle: String? = null, + description: String? = null, + authenticator: BiometricAuthenticators = BiometricAuthenticators.STRONG, + enableDeviceCredentialFallback: Boolean = false, + negativeButtonText: String = "Cancel" + ): Boolean { + val biometricManager = BiometricManager.from(activity) + val authenticators = if (enableDeviceCredentialFallback) { + authenticator.value or BiometricAuthenticators.DEVICE_CREDENTIAL.value + } else { + authenticator.value + } + + val authenticatorStatus = biometricManager.canAuthenticate(authenticators) + authenticateBeforeDecrypt = authenticatorStatus == BiometricManager.BIOMETRIC_SUCCESS + if (!authenticateBeforeDecrypt) { + logAuthenticatorErrorStatus(authenticatorStatus) return false } - if (resultCode == Activity.RESULT_OK) { - continueGetCredentials(scope, minTtl, emptyMap(), emptyMap(), forceRefresh, decryptCallback!!) - } else { - decryptCallback!!.onFailure(CredentialsManagerException("The user didn't pass the authentication challenge.")) - decryptCallback = null + + val bioMetricPromptBuilder = BiometricPrompt.PromptInfo.Builder() + .setTitle(title) + .setSubtitle(subtitle) + .setDescription(description) + .setAllowedAuthenticators(authenticators) + if (!enableDeviceCredentialFallback) { + bioMetricPromptBuilder.setNegativeButtonText(negativeButtonText) } + biometricPromptInfo = bioMetricPromptBuilder.build() + biometricPrompt = BiometricPrompt(activity, serialExecutor, authenticationCallback) return true } + private fun logAuthenticatorErrorStatus(authenticatorStatus: Int) { + val errorMessages = mapOf( + BIOMETRIC_ERROR_HW_UNAVAILABLE to "The hardware is unavailable. Try again later.", + BIOMETRIC_ERROR_NONE_ENROLLED to "The user does not have any biometrics enrolled.", + BIOMETRIC_ERROR_NO_HARDWARE to "There is no biometric hardware.", + BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED to "A security vulnerability has been discovered and the sensor is unavailable until a security update has addressed this issue." + ) + + val errorMessage = errorMessages[authenticatorStatus] + if (errorMessage != null) { + Log.e(TAG, errorMessage) + } + } + + /** * Saves the given credentials in the Storage. * @@ -192,6 +222,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT * to use on the next call. We clear any existing credentials so #hasValidCredentials returns * a true value. Retrying this operation will succeed. */ + // suspect this is not something we do in Auth0.swift clearCredentials() throw CredentialsManagerException( "A change on the Lock Screen security settings have deemed the encryption keys invalid and have been recreated. Please try saving the credentials again.", @@ -446,17 +477,18 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT callback.onFailure(CredentialsManagerException("No Credentials were previously set.")) return } + if (authenticateBeforeDecrypt) { Log.d( TAG, "Authentication is required to read the Credentials. Showing the LockScreen." ) + // why did we left headers and parameters when we are authenticating before decrypting ? decryptCallback = callback this.scope = scope this.minTtl = minTtl this.forceRefresh = forceRefresh - activityResultContract?.launch(authIntent) - ?: activity?.startActivityForResult(authIntent, authenticationRequestCode) + biometricPromptInfo?.let { biometricPrompt?.authenticate(it) } return } continueGetCredentials(scope, minTtl, parameters, headers, forceRefresh, callback) @@ -535,6 +567,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT decryptCallback = null return@execute } catch (e: CryptoException) { + // suspect this is not something we do in Auth0.swift //If keys were invalidated, existing credentials will not be recoverable. clearCredentials() callback.onFailure( @@ -576,6 +609,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT return@execute } if (credentials.refreshToken == null) { + // ideally we will have to change the log to say, token expired or token needs to be refreshed but no refresh token exists to do so. callback.onFailure(CredentialsManagerException("No Credentials were previously set.")) decryptCallback = null return@execute @@ -641,8 +675,9 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT callback.onSuccess(freshCredentials) } catch (error: CredentialsManagerException) { val exception = CredentialsManagerException( - "An error occurred while saving the refreshed Credentials.", error) - if(error.cause is IncompatibleDeviceException || error.cause is CryptoException) { + "An error occurred while saving the refreshed Credentials.", error + ) + if (error.cause is IncompatibleDeviceException || error.cause is CryptoException) { exception.refreshedCredentials = freshCredentials } callback.onFailure(exception) @@ -655,6 +690,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT private val TAG = SecureCredentialsManager::class.java.simpleName private const val KEY_CREDENTIALS = "com.auth0.credentials" private const val KEY_EXPIRES_AT = "com.auth0.credentials_access_token_expires_at" + // This is no longer used as we get the credentials expiry from the access token only, // but we still store it so users can rollback to versions where it is required. private const val LEGACY_KEY_CACHE_EXPIRES_AT = "com.auth0.credentials_expires_at" From 0c7cb16d63e93709af2cd3162212694d26aa9e77 Mon Sep 17 00:00:00 2001 From: Sai Venkat Desu Date: Thu, 6 Jun 2024 19:44:39 +0530 Subject: [PATCH 02/30] feat: refined support for biometric manager Signed-off-by: Sai Venkat Desu --- .../biometrics/BiometricAuthenticators.kt | 9 - .../storage/LocalAuthenticationManager.kt | 91 +++++++ .../storage/LocalAuthenticationOptions.kt | 52 ++++ .../storage/SecureCredentialsManager.kt | 229 ++++-------------- .../com/auth0/sample/DatabaseLoginFragment.kt | 7 +- 5 files changed, 201 insertions(+), 187 deletions(-) delete mode 100644 auth0/src/main/java/com/auth0/android/authentication/biometrics/BiometricAuthenticators.kt create mode 100644 auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationManager.kt create mode 100644 auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationOptions.kt diff --git a/auth0/src/main/java/com/auth0/android/authentication/biometrics/BiometricAuthenticators.kt b/auth0/src/main/java/com/auth0/android/authentication/biometrics/BiometricAuthenticators.kt deleted file mode 100644 index b9246a7b..00000000 --- a/auth0/src/main/java/com/auth0/android/authentication/biometrics/BiometricAuthenticators.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.auth0.android.authentication.biometrics - - -import android.hardware.biometrics.BiometricManager.Authenticators -public enum class BiometricAuthenticators private constructor (public val value: Int) { - STRONG (Authenticators.BIOMETRIC_STRONG), - WEAK (Authenticators.BIOMETRIC_WEAK), - DEVICE_CREDENTIAL (Authenticators.DEVICE_CREDENTIAL); -} \ No newline at end of file diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationManager.kt new file mode 100644 index 00000000..1f1bb3c4 --- /dev/null +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationManager.kt @@ -0,0 +1,91 @@ +package com.auth0.android.authentication.storage + +import android.util.Log +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricPrompt +import androidx.fragment.app.FragmentActivity +import com.auth0.android.callback.Callback +import java.util.concurrent.Executor + + +internal class LocalAuthenticationManager( + private val activity: FragmentActivity, + private val authenticationOptions: LocalAuthenticationOptions, + private val executor: Executor, +) { + private val biometricManager = BiometricManager.from(activity) + + fun authenticate(resultCallback: Callback) { + val authenticationLevels = if (authenticationOptions.enableDeviceCredentialFallback) { + authenticationOptions.authenticationLevel.value or AuthenticationLevel.DEVICE_CREDENTIAL.value + } else { + authenticationOptions.authenticationLevel.value + } + + // canAuthenticate API doesn't work as expected on all the API levels, need to work on this. + val isAuthenticationPossible = biometricManager.canAuthenticate(authenticationLevels) + if (isAuthenticationPossible != BiometricManager.BIOMETRIC_SUCCESS) { + logAuthenticatorErrorStatus(isAuthenticationPossible) + resultCallback.onFailure(CredentialsManagerException("Supplied Authenticators are not possible")) + return + } + + val bioMetricPromptInfoBuilder = BiometricPrompt.PromptInfo.Builder().apply { + authenticationOptions.run { + setTitle(title) + setSubtitle(subtitle) + setDescription(description) + if (!enableDeviceCredentialFallback) { + setNegativeButtonText(negativeButtonText) + } + } + setAllowedAuthenticators(authenticationLevels) + } + + val biometricPromptInfo = bioMetricPromptInfoBuilder.build() + val biometricPrompt = BiometricPrompt( + activity, + executor, + biometricPromptAuthenticationCallback(resultCallback) + ) + biometricPrompt.authenticate(biometricPromptInfo) + } + + private fun logAuthenticatorErrorStatus(authenticatorStatus: Int) { + val errorMessages = mapOf( + BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE to "The hardware is unavailable. Try again later.", + BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED to "The user does not have any biometrics enrolled.", + BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE to "There is no biometric hardware.", + BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED to "A security vulnerability has been discovered and the sensor is unavailable until a security update has addressed this issue." + ) + + val errorMessage = errorMessages[authenticatorStatus] + if (errorMessage != null) { + Log.e(TAG, errorMessage) + } + } + + private val biometricPromptAuthenticationCallback = + { callback: Callback -> + object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + callback.onSuccess(true) + } + + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + callback.onFailure(CredentialsManagerException("Biometrics Authentication Failed with error code $errorCode due to $errString")) + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + callback.onFailure(CredentialsManagerException("The user didn't pass the authentication challenge.")) + } + } + } + + internal companion object { + private val TAG = LocalAuthenticationManager::class.java.simpleName + } +} \ No newline at end of file diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationOptions.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationOptions.kt new file mode 100644 index 00000000..62930e83 --- /dev/null +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationOptions.kt @@ -0,0 +1,52 @@ +package com.auth0.android.authentication.storage + +import androidx.biometric.BiometricManager + + +public class LocalAuthenticationOptions private constructor( + public val title: String, + public val subtitle: String?, + public val description: String?, + public val authenticationLevel: AuthenticationLevel, + public val enableDeviceCredentialFallback: Boolean, + public val negativeButtonText: String +) { + public class Builder( + private var title: String? = null, + private var subtitle: String? = null, + private var description: String? = null, + private var authenticator: AuthenticationLevel = AuthenticationLevel.STRONG, + private var enableDeviceCredentialFallback: Boolean = false, + private var negativeButtonText: String = "Cancel" + ) { + + public fun title(title: String): Builder = apply { this.title = title } + public fun subtitle(subtitle: String?): Builder = apply { this.subtitle = subtitle } + public fun description(description: String?): Builder = + apply { this.description = description } + + public fun authenticator(authenticator: AuthenticationLevel): Builder = + apply { this.authenticator = authenticator } + + public fun enableDeviceCredentialFallback(enableDeviceCredentialFallback: Boolean): Builder = + apply { this.enableDeviceCredentialFallback = enableDeviceCredentialFallback } + + public fun negativeButtonText(negativeButtonText: String): Builder = + apply { this.negativeButtonText = negativeButtonText } + + public fun build(): LocalAuthenticationOptions = LocalAuthenticationOptions( + title ?: throw IllegalArgumentException("Title must be provided"), + subtitle, + description, + authenticator, + enableDeviceCredentialFallback, + negativeButtonText + ) + } +} + +public enum class AuthenticationLevel(public val value: Int) { + STRONG(BiometricManager.Authenticators.BIOMETRIC_STRONG), + WEAK(BiometricManager.Authenticators.BIOMETRIC_WEAK), + DEVICE_CREDENTIAL(BiometricManager.Authenticators.DEVICE_CREDENTIAL); +} \ No newline at end of file diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt index 87c5b1f8..67e8bdb0 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt @@ -2,17 +2,10 @@ package com.auth0.android.authentication.storage import android.app.Activity import android.content.Context -import android.hardware.biometrics.BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -import android.hardware.biometrics.BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -import android.hardware.biometrics.BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -import android.hardware.biometrics.BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED import android.text.TextUtils import android.util.Base64 import android.util.Log -import androidx.biometric.BiometricPrompt -import androidx.annotation.IntRange import androidx.annotation.VisibleForTesting -import androidx.biometric.BiometricManager import androidx.fragment.app.FragmentActivity import com.auth0.android.Auth0Exception import com.auth0.android.authentication.AuthenticationAPIClient @@ -27,7 +20,6 @@ import java.util.concurrent.Executor import java.util.concurrent.Executors import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException -import com.auth0.android.authentication.biometrics.BiometricAuthenticators /** * A safer alternative to the [CredentialsManager] class. A combination of RSA and AES keys is used to keep the values secure. @@ -42,43 +34,6 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT ) : BaseCredentialsManager(apiClient, storage, jwtDecoder) { private val gson: Gson = GsonProvider.gson - //Changeable by the user - private var authenticateBeforeDecrypt: Boolean - private var biometricPromptInfo: BiometricPrompt.PromptInfo? = null - private var biometricPrompt: BiometricPrompt? = null - - //State for retrying operations - private var decryptCallback: Callback? = null - private var scope: String? = null - private var minTtl = 0 - private var forceRefresh = false - - - private val authenticationCallback: BiometricPrompt.AuthenticationCallback = - object : BiometricPrompt.AuthenticationCallback() { - override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { - super.onAuthenticationSucceeded(result) - decryptCallback?.let { - continueGetCredentials( - scope, minTtl, emptyMap(), emptyMap(), forceRefresh, - decryptCallback!! - ) - } - } - - override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { - super.onAuthenticationError(errorCode, errString) - decryptCallback!!.onFailure(CredentialsManagerException("Biometrics Authentication Failed with error code ${errorCode} due to ${errString}")) - decryptCallback = null - - } - - override fun onAuthenticationFailed() { - super.onAuthenticationFailed() - decryptCallback!!.onFailure(CredentialsManagerException("The user didn't pass the authentication challenge.")) - decryptCallback = null - } - } /** * Creates a new SecureCredentialsManager to handle Credentials @@ -99,90 +54,6 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT Executors.newSingleThreadExecutor() ) - /** - * Require the user to authenticate using the configured LockScreen before accessing the credentials. - * This method MUST be called in [Activity.onCreate]. This feature is disabled by default and will - * only work if the user has configured a secure LockScreen (PIN, Pattern, Password or Fingerprint). - * - * If the activity passed as first argument is a subclass of ComponentActivity, the authentication result - * will be handled internally using "Activity Results API" which should be called from the main thread. - * Otherwise, your activity must override the [Activity.onActivityResult] method - * and call [SecureCredentialsManager.checkAuthenticationResult] with the received parameters. - * - * @param activity a valid activity context. Will be used in the authentication request to launch a LockScreen intent. - * @param requestCode the request code to use in the authentication request. Must be a value between 1 and 255. - * @param title the text to use as title in the authentication screen. Passing null will result in using the OS's default value. - * @param description the text to use as description in the authentication screen. On some Android versions it might not be shown. Passing null will result in using the OS's default value. - * @return whether this device supports requiring authentication or not. This result can be ignored safely. - */ - public fun requireAuthentication( - activity: Activity, - @IntRange(from = 1, to = 255) requestCode: Int, - title: String?, - description: String? - ): Boolean { - if (activity is FragmentActivity) { - return requireAuthentication(activity = activity, title = "Biometric Authentication") - } else { - Log.e(TAG, "requireAuthentication() needs an activity of type FragmentActivity to support Biometrics Authentication") - return false - } - } - - - // i feel its better we return back an enum so that they can act on top of it, rather than skimming through logs - // for eg: if they tried for strong, but user has not enrolled then they can direct them to enroll or something like that - - public fun requireAuthentication( - activity: FragmentActivity, - title: String, - subtitle: String? = null, - description: String? = null, - authenticator: BiometricAuthenticators = BiometricAuthenticators.STRONG, - enableDeviceCredentialFallback: Boolean = false, - negativeButtonText: String = "Cancel" - ): Boolean { - val biometricManager = BiometricManager.from(activity) - val authenticators = if (enableDeviceCredentialFallback) { - authenticator.value or BiometricAuthenticators.DEVICE_CREDENTIAL.value - } else { - authenticator.value - } - - val authenticatorStatus = biometricManager.canAuthenticate(authenticators) - authenticateBeforeDecrypt = authenticatorStatus == BiometricManager.BIOMETRIC_SUCCESS - if (!authenticateBeforeDecrypt) { - logAuthenticatorErrorStatus(authenticatorStatus) - return false - } - - val bioMetricPromptBuilder = BiometricPrompt.PromptInfo.Builder() - .setTitle(title) - .setSubtitle(subtitle) - .setDescription(description) - .setAllowedAuthenticators(authenticators) - if (!enableDeviceCredentialFallback) { - bioMetricPromptBuilder.setNegativeButtonText(negativeButtonText) - } - biometricPromptInfo = bioMetricPromptBuilder.build() - biometricPrompt = BiometricPrompt(activity, serialExecutor, authenticationCallback) - return true - } - - private fun logAuthenticatorErrorStatus(authenticatorStatus: Int) { - val errorMessages = mapOf( - BIOMETRIC_ERROR_HW_UNAVAILABLE to "The hardware is unavailable. Try again later.", - BIOMETRIC_ERROR_NONE_ENROLLED to "The user does not have any biometrics enrolled.", - BIOMETRIC_ERROR_NO_HARDWARE to "There is no biometric hardware.", - BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED to "A security vulnerability has been discovered and the sensor is unavailable until a security update has addressed this issue." - ) - - val errorMessage = errorMessages[authenticatorStatus] - if (errorMessage != null) { - Log.e(TAG, errorMessage) - } - } - /** * Saves the given credentials in the Storage. @@ -448,24 +319,25 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT getCredentials(scope, minTtl, parameters, mapOf(), forceRefresh, callback) } - /** - * Tries to obtain the credentials from the Storage. The callback's [Callback.onSuccess] method will be called with the result. - * If something unexpected happens, the [Callback.onFailure] method will be called with the error. Some devices are not compatible - * at all with the cryptographic implementation and will have [CredentialsManagerException.isDeviceIncompatible] return true. - * - * - * If a LockScreen is setup and [SecureCredentialsManager.requireAuthentication] was called, the user will be asked to authenticate before accessing - * the credentials. Your activity must override the [Activity.onActivityResult] method and call - * [SecureCredentialsManager.checkAuthenticationResult] with the received values. - * - * @param scope the scope to request for the access token. If null is passed, the previous scope will be kept. - * @param minTtl the minimum time in seconds that the access token should last before expiration. - * @param parameters additional parameters to send in the request to refresh expired credentials. - * @param headers additional headers to send in the request to refresh expired credentials. - * @param forceRefresh this will avoid returning the existing credentials and retrieves a new one even if valid credentials exist. - * @param callback the callback to receive the result in. - */ - public fun getCredentials( + private val localAuthenticationResultCallback = + { scope: String?, minTtl: Int, parameters: Map, headers: Map, forceRefresh: Boolean, callback: Callback -> + object : Callback { + override fun onSuccess(result: Boolean) { + getCredentials( + scope, minTtl, parameters, headers, forceRefresh, + callback + ) + } + + override fun onFailure(error: CredentialsManagerException) { + callback.onFailure(error) + } + } + } + + public fun getCredentialsWithAuthentication( + activity: FragmentActivity, + authenticationOptions: LocalAuthenticationOptions, scope: String?, minTtl: Int, parameters: Map, @@ -473,25 +345,28 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT forceRefresh: Boolean, callback: Callback ) { - if (!hasValidCredentials(minTtl.toLong())) { - callback.onFailure(CredentialsManagerException("No Credentials were previously set.")) + + if (!isBiometricManagerPackageAvailable()) { + callback.onFailure(CredentialsManagerException("BiometricManager package is not available on classpath, please add it to perform authentication before retrieving credentials")) return } - if (authenticateBeforeDecrypt) { - Log.d( - TAG, - "Authentication is required to read the Credentials. Showing the LockScreen." + val localAuthenticationManager = LocalAuthenticationManager( + activity, + authenticationOptions, + serialExecutor + ) + + localAuthenticationManager.authenticate( + localAuthenticationResultCallback( + scope, + minTtl, + parameters, + headers, + forceRefresh, + callback ) - // why did we left headers and parameters when we are authenticating before decrypting ? - decryptCallback = callback - this.scope = scope - this.minTtl = minTtl - this.forceRefresh = forceRefresh - biometricPromptInfo?.let { biometricPrompt?.authenticate(it) } - return - } - continueGetCredentials(scope, minTtl, parameters, headers, forceRefresh, callback) + ) } /** @@ -536,7 +411,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT (canRefresh == null || !canRefresh)) } - private fun continueGetCredentials( + private fun getCredentials( scope: String?, minTtl: Int, parameters: Map, @@ -544,11 +419,15 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT forceRefresh: Boolean, callback: Callback ) { + + if (!hasValidCredentials(minTtl.toLong())) { + callback.onFailure(CredentialsManagerException("No Credentials were previously set.")) + return + } serialExecutor.execute { val encryptedEncoded = storage.retrieveString(KEY_CREDENTIALS) if (encryptedEncoded.isNullOrBlank()) { callback.onFailure(CredentialsManagerException("No Credentials were previously set.")) - decryptCallback = null return@execute } val encrypted = Base64.decode(encryptedEncoded, Base64.DEFAULT) @@ -564,7 +443,6 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT ), e ) ) - decryptCallback = null return@execute } catch (e: CryptoException) { // suspect this is not something we do in Auth0.swift @@ -577,7 +455,6 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT e ) ) - decryptCallback = null return@execute } val bridgeCredentials = gson.fromJson(json, OptionalCredentials::class.java) @@ -598,20 +475,17 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT TextUtils.isEmpty(credentials.accessToken) && TextUtils.isEmpty(credentials.idToken) if (hasEmptyCredentials) { callback.onFailure(CredentialsManagerException("No Credentials were previously set.")) - decryptCallback = null return@execute } val willAccessTokenExpire = willExpire(expiresAt, minTtl.toLong()) val scopeChanged = hasScopeChanged(credentials.scope, scope) if (!forceRefresh && !willAccessTokenExpire && !scopeChanged) { callback.onSuccess(credentials) - decryptCallback = null return@execute } if (credentials.refreshToken == null) { // ideally we will have to change the log to say, token expired or token needs to be refreshed but no refresh token exists to do so. callback.onFailure(CredentialsManagerException("No Credentials were previously set.")) - decryptCallback = null return@execute } Log.d(TAG, "Credentials have expired. Renewing them now...") @@ -644,7 +518,6 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT ) ) callback.onFailure(wrongTtlException) - decryptCallback = null return@execute } @@ -666,7 +539,6 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT error ) ) - decryptCallback = null return@execute } @@ -682,7 +554,16 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT } callback.onFailure(exception) } - decryptCallback = null + } + } + + private fun isBiometricManagerPackageAvailable(): Boolean { + return try { + // Attempt to load a class from the androidx.biometric package + Class.forName("androidx.biometric.BiometricManager") + true // If successful, package is available + } catch (e: ClassNotFoundException) { + false // If ClassNotFoundException is thrown, package is not available } } @@ -697,8 +578,4 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT private const val KEY_CAN_REFRESH = "com.auth0.credentials_can_refresh" private const val KEY_ALIAS = "com.auth0.key" } - - init { - authenticateBeforeDecrypt = false - } } \ No newline at end of file diff --git a/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt b/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt index c73223d1..33850819 100644 --- a/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt +++ b/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt @@ -8,7 +8,9 @@ import androidx.fragment.app.Fragment import com.auth0.android.Auth0 import com.auth0.android.authentication.AuthenticationAPIClient import com.auth0.android.authentication.AuthenticationException +import com.auth0.android.authentication.storage.AuthenticationLevel import com.auth0.android.authentication.storage.CredentialsManagerException +import com.auth0.android.authentication.storage.LocalAuthenticationOptions import com.auth0.android.authentication.storage.SecureCredentialsManager import com.auth0.android.authentication.storage.SharedPreferencesStorage import com.auth0.android.callback.Callback @@ -247,7 +249,8 @@ class DatabaseLoginFragment : Fragment() { } private fun getCreds() { - credentialsManager.getCredentials(object : Callback { + val localAuthenticationOptions = LocalAuthenticationOptions.Builder().title("Biometric").description("description").authenticator(AuthenticationLevel.STRONG).negativeButtonText("Cancel").build() + credentialsManager.getCredentialsWithAuthentication(requireActivity(), localAuthenticationOptions, null, 300, emptyMap(), emptyMap(), false, object : Callback { override fun onSuccess(result: Credentials) { Snackbar.make( requireView(), @@ -259,7 +262,7 @@ class DatabaseLoginFragment : Fragment() { override fun onFailure(error: CredentialsManagerException) { Snackbar.make(requireView(), "${error.message}", Snackbar.LENGTH_LONG).show() } - }) + } ) } private suspend fun getCredsAsync() { From 1c4beca49c9e70d2ebcad527c082903fc6e9acb4 Mon Sep 17 00:00:00 2001 From: Sai Venkat Desu Date: Mon, 10 Jun 2024 17:34:54 +0530 Subject: [PATCH 03/30] feat: better handling of the exceptions thrown from CredentialsManager Signed-off-by: Sai Venkat Desu --- .../storage/CredentialsManager.kt | 10 +- .../storage/CredentialsManagerException.kt | 111 +++++++++++++++++- .../storage/LocalAuthenticationManager.kt | 39 +++--- .../storage/SecureCredentialsManager.kt | 47 +++----- sample/build.gradle | 3 + 5 files changed, 154 insertions(+), 56 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt index 2efe8537..7f5a20b6 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt @@ -42,7 +42,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting */ override fun saveCredentials(credentials: Credentials) { if (TextUtils.isEmpty(credentials.accessToken) && TextUtils.isEmpty(credentials.idToken)) { - throw CredentialsManagerException("Credentials must have a valid date of expiration and a valid access_token or id_token value.") + throw CredentialsManagerException.INVALID_CREDENTIALS } storage.store(KEY_ACCESS_TOKEN, credentials.accessToken) storage.store(KEY_REFRESH_TOKEN, credentials.refreshToken) @@ -260,7 +260,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting val hasEmptyCredentials = TextUtils.isEmpty(accessToken) && TextUtils.isEmpty(idToken) || expiresAt == null if (hasEmptyCredentials) { - callback.onFailure(CredentialsManagerException("No Credentials were previously set.")) + callback.onFailure(CredentialsManagerException.NO_CREDENTIALS) return@execute } val willAccessTokenExpire = willExpire(expiresAt!!, minTtl.toLong()) @@ -279,7 +279,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting return@execute } if (refreshToken == null) { - callback.onFailure(CredentialsManagerException("Credentials need to be renewed but no Refresh Token is available to renew them.")) + callback.onFailure(CredentialsManagerException.NO_REFRESH_TOKEN) return@execute } val request = authenticationClient.renewAuth(refreshToken) @@ -299,6 +299,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting if (willAccessTokenExpire) { val tokenLifetime = (expiresAt - currentTimeInMillis - minTtl * 1000) / -1000 val wrongTtlException = CredentialsManagerException( + CredentialsManagerException.Code.LARGE_MIN_TTL, String.format( Locale.getDefault(), "The lifetime of the renewed Access Token (%d) is less than the minTTL requested (%d). Increase the 'Token Expiration' setting of your Auth0 API in the dashboard, or request a lower minTTL.", @@ -326,7 +327,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting } catch (error: AuthenticationException) { callback.onFailure( CredentialsManagerException( - "An error occurred while trying to use the Refresh Token to renew the Credentials.", + CredentialsManagerException.Code.RENEW_FAILED, error ) ) @@ -394,6 +395,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting private const val KEY_TOKEN_TYPE = "com.auth0.token_type" private const val KEY_EXPIRES_AT = "com.auth0.expires_at" private const val KEY_SCOPE = "com.auth0.scope" + // This is no longer used as we get the credentials expiry from the access token only, // but we still store it so users can rollback to versions where it is required. private const val LEGACY_KEY_CACHE_EXPIRES_AT = "com.auth0.cache_expires_at" diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManagerException.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManagerException.kt index 2858b958..df664176 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManagerException.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManagerException.kt @@ -6,10 +6,113 @@ import com.auth0.android.result.Credentials /** * Represents an error raised by the [CredentialsManager]. */ -public class CredentialsManagerException internal constructor( - message: String, - cause: Throwable? = null -) : Auth0Exception(message, cause) { +public class CredentialsManagerException : + Auth0Exception { + + internal enum class Code { + INVALID_CREDENTIALS, + NO_CREDENTIALS, + NO_REFRESH_TOKEN, + RENEW_FAILED, + STORE_FAILED, + BIOMETRICS_FAILED, + REVOKE_FAILED, + LARGE_MIN_TTL, + INCOMPATIBLE_DEVICE, + CRYPTO_EXCEPTION, + BIOMETRICS_PACKAGE_NOT_FOUND, + BIOMETRIC_STATUS_UNKNOWN, + BIOMETRIC_AUTHENTICATION_CHECK_FAILED, + BIOMETRIC_ERROR_UNSUPPORTED, + BIOMETRIC_ERROR_HW_UNAVAILABLE, + BIOMETRIC_ERROR_NONE_ENROLLED, + BIOMETRIC_ERROR_NO_HARDWARE, + BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED, + } + + private var code: Code? + + + internal constructor(code: Code, cause: Throwable? = null) : this( + code, + getMessage(code), + cause + ) + + internal constructor(code: Code, message: String, cause: Throwable? = null) : super( + message, + cause + ) { + this.code = code + } + + public companion object { + + public val INVALID_CREDENTIALS: CredentialsManagerException = + CredentialsManagerException(Code.INVALID_CREDENTIALS) + public val NO_CREDENTIALS: CredentialsManagerException = + CredentialsManagerException(Code.NO_CREDENTIALS) + public val NO_REFRESH_TOKEN: CredentialsManagerException = + CredentialsManagerException(Code.NO_REFRESH_TOKEN) + public val RENEW_FAILED: CredentialsManagerException = + CredentialsManagerException(Code.RENEW_FAILED) + public val STORE_FAILED: CredentialsManagerException = + CredentialsManagerException(Code.STORE_FAILED) + public val BIOMETRICS_FAILED: CredentialsManagerException = + CredentialsManagerException(Code.BIOMETRICS_FAILED) + public val REVOKE_FAILED: CredentialsManagerException = + CredentialsManagerException(Code.REVOKE_FAILED) + public val LARGE_MIN_TTL: CredentialsManagerException = + CredentialsManagerException(Code.LARGE_MIN_TTL) + public val INCOMPATIBLE_DEVICE: CredentialsManagerException = + CredentialsManagerException(Code.INCOMPATIBLE_DEVICE) + public val CRYPTO_EXCEPTION: CredentialsManagerException = + CredentialsManagerException(Code.CRYPTO_EXCEPTION) + public val BIOMETRICS_PACKAGE_NOT_FOUND: CredentialsManagerException = + CredentialsManagerException(Code.BIOMETRICS_PACKAGE_NOT_FOUND) + public val BIOMETRIC_STATUS_UNKNOWN: CredentialsManagerException = + CredentialsManagerException(Code.BIOMETRIC_STATUS_UNKNOWN) + public val BIOMETRIC_ERROR_UNSUPPORTED: CredentialsManagerException = + CredentialsManagerException(Code.BIOMETRIC_ERROR_UNSUPPORTED) + public val BIOMETRIC_ERROR_HW_UNAVAILABLE: CredentialsManagerException = + CredentialsManagerException(Code.BIOMETRIC_ERROR_HW_UNAVAILABLE) + public val BIOMETRIC_ERROR_NONE_ENROLLED: CredentialsManagerException = + CredentialsManagerException(Code.BIOMETRIC_ERROR_NONE_ENROLLED) + public val BIOMETRIC_ERROR_NO_HARDWARE: CredentialsManagerException = + CredentialsManagerException(Code.BIOMETRIC_ERROR_NO_HARDWARE) + public val BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED: CredentialsManagerException = + CredentialsManagerException(Code.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED) + public val BIOMETRIC_AUTHENTICATION_CHECK_FAILED: CredentialsManagerException = + CredentialsManagerException(Code.BIOMETRIC_AUTHENTICATION_CHECK_FAILED) + + + private fun getMessage(code: Code): String { + return when (code) { + Code.INVALID_CREDENTIALS -> "Credentials must have a valid access_token or id_token value." + Code.NO_CREDENTIALS -> "No Credentials were previously set." + Code.NO_REFRESH_TOKEN -> "Credentials need to be renewed but no Refresh Token is available to renew them." + Code.RENEW_FAILED -> "An error occurred while trying to use the Refresh Token to renew the Credentials." + Code.STORE_FAILED -> "An error occurred while saving the refreshed Credentials." + Code.BIOMETRICS_FAILED -> "The user didn't pass the authentication challenge." + Code.REVOKE_FAILED -> "The revocation of the refresh token failed." + Code.LARGE_MIN_TTL -> "The minTTL requested is greater than the lifetime of the renewed access token. Request a lower minTTL or increase the 'Token Expiration' value in the settings page of your Auth0 API." + Code.INCOMPATIBLE_DEVICE -> String.format( + "This device is not compatible with the %s class.", + SecureCredentialsManager::class.java.simpleName + ) + + Code.CRYPTO_EXCEPTION -> "A change on the Lock Screen security settings have deemed the encryption keys invalid and have been recreated. Please try saving the credentials again." + Code.BIOMETRICS_PACKAGE_NOT_FOUND -> "Package androidx.biometric:biometric is not found. Please add it to your dependencies to enable authentication before retrieving credentials." + Code.BIOMETRIC_STATUS_UNKNOWN -> "Unable to determine whether the user can authenticate." + Code.BIOMETRIC_ERROR_UNSUPPORTED -> "Cannot authenticate because the specified options are incompatible with the current Android version." + Code.BIOMETRIC_ERROR_HW_UNAVAILABLE -> "Cannot authenticate because the hardware is unavailable. Try again later." + Code.BIOMETRIC_ERROR_NONE_ENROLLED -> "Cannot authenticate because no biometric or device credential is enrolled for the user." + Code.BIOMETRIC_ERROR_NO_HARDWARE -> "Cannot authenticate because there is no suitable hardware (e.g. no biometric sensor or no keyguard)." + Code.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> "Cannot authenticate because a security vulnerability has been discovered with one or more hardware sensors. The affected sensor(s) are unavailable until a security update has addressed the issue." + Code.BIOMETRIC_AUTHENTICATION_CHECK_FAILED -> "Failed to determine if the user can authenticate with an authenticator that meets the given requirements" + } + } + } /** * Returns true when this Android device doesn't support the cryptographic algorithms used diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationManager.kt index 1f1bb3c4..8f64578f 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationManager.kt @@ -1,6 +1,5 @@ package com.auth0.android.authentication.storage -import android.util.Log import androidx.biometric.BiometricManager import androidx.biometric.BiometricPrompt import androidx.fragment.app.FragmentActivity @@ -25,8 +24,11 @@ internal class LocalAuthenticationManager( // canAuthenticate API doesn't work as expected on all the API levels, need to work on this. val isAuthenticationPossible = biometricManager.canAuthenticate(authenticationLevels) if (isAuthenticationPossible != BiometricManager.BIOMETRIC_SUCCESS) { - logAuthenticatorErrorStatus(isAuthenticationPossible) - resultCallback.onFailure(CredentialsManagerException("Supplied Authenticators are not possible")) + resultCallback.onFailure( + generateExceptionFromAuthenticationPossibilityError( + isAuthenticationPossible + ) + ) return } @@ -51,18 +53,18 @@ internal class LocalAuthenticationManager( biometricPrompt.authenticate(biometricPromptInfo) } - private fun logAuthenticatorErrorStatus(authenticatorStatus: Int) { - val errorMessages = mapOf( - BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE to "The hardware is unavailable. Try again later.", - BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED to "The user does not have any biometrics enrolled.", - BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE to "There is no biometric hardware.", - BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED to "A security vulnerability has been discovered and the sensor is unavailable until a security update has addressed this issue." - ) - val errorMessage = errorMessages[authenticatorStatus] - if (errorMessage != null) { - Log.e(TAG, errorMessage) - } + private fun generateExceptionFromAuthenticationPossibilityError(errorCode: Int): CredentialsManagerException { + val exceptionCode = mapOf( + BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE to CredentialsManagerException.BIOMETRIC_ERROR_HW_UNAVAILABLE, + BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED to CredentialsManagerException.BIOMETRIC_ERROR_NONE_ENROLLED, + BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE to CredentialsManagerException.BIOMETRIC_ERROR_NO_HARDWARE, + BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED to CredentialsManagerException.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED, + BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED to CredentialsManagerException.BIOMETRIC_ERROR_UNSUPPORTED, + BiometricManager.BIOMETRIC_STATUS_UNKNOWN to CredentialsManagerException.BIOMETRIC_STATUS_UNKNOWN + ) + return exceptionCode[errorCode] + ?: CredentialsManagerException.BIOMETRIC_AUTHENTICATION_CHECK_FAILED } private val biometricPromptAuthenticationCallback = @@ -75,12 +77,17 @@ internal class LocalAuthenticationManager( override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { super.onAuthenticationError(errorCode, errString) - callback.onFailure(CredentialsManagerException("Biometrics Authentication Failed with error code $errorCode due to $errString")) + callback.onFailure( + CredentialsManagerException( + CredentialsManagerException.Code.BIOMETRICS_FAILED, + "Biometrics Authentication Failed with error code $errorCode due to $errString" + ) + ) } override fun onAuthenticationFailed() { super.onAuthenticationFailed() - callback.onFailure(CredentialsManagerException("The user didn't pass the authentication challenge.")) + callback.onFailure(CredentialsManagerException.BIOMETRICS_FAILED) } } } diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt index 67e8bdb0..f476195c 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt @@ -66,7 +66,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT @Synchronized override fun saveCredentials(credentials: Credentials) { if (TextUtils.isEmpty(credentials.accessToken) && TextUtils.isEmpty(credentials.idToken)) { - throw CredentialsManagerException("Credentials must have a valid date of expiration and a valid access_token or id_token value.") + throw CredentialsManagerException.INVALID_CREDENTIALS } val json = gson.toJson(credentials) val canRefresh = !TextUtils.isEmpty(credentials.refreshToken) @@ -82,10 +82,8 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT storage.store(KEY_CAN_REFRESH, canRefresh) } catch (e: IncompatibleDeviceException) { throw CredentialsManagerException( - String.format( - "This device is not compatible with the %s class.", - SecureCredentialsManager::class.java.simpleName - ), e + CredentialsManagerException.Code.INCOMPATIBLE_DEVICE, + e ) } catch (e: CryptoException) { /* @@ -95,10 +93,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT */ // suspect this is not something we do in Auth0.swift clearCredentials() - throw CredentialsManagerException( - "A change on the Lock Screen security settings have deemed the encryption keys invalid and have been recreated. Please try saving the credentials again.", - e - ) + throw CredentialsManagerException(CredentialsManagerException.Code.CRYPTO_EXCEPTION, e) } } @@ -347,7 +342,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT ) { if (!isBiometricManagerPackageAvailable()) { - callback.onFailure(CredentialsManagerException("BiometricManager package is not available on classpath, please add it to perform authentication before retrieving credentials")) + callback.onFailure(CredentialsManagerException.BIOMETRICS_PACKAGE_NOT_FOUND) return } @@ -421,13 +416,13 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT ) { if (!hasValidCredentials(minTtl.toLong())) { - callback.onFailure(CredentialsManagerException("No Credentials were previously set.")) + callback.onFailure(CredentialsManagerException.NO_CREDENTIALS) return } serialExecutor.execute { val encryptedEncoded = storage.retrieveString(KEY_CREDENTIALS) if (encryptedEncoded.isNullOrBlank()) { - callback.onFailure(CredentialsManagerException("No Credentials were previously set.")) + callback.onFailure(CredentialsManagerException.NO_CREDENTIALS) return@execute } val encrypted = Base64.decode(encryptedEncoded, Base64.DEFAULT) @@ -437,10 +432,8 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT } catch (e: IncompatibleDeviceException) { callback.onFailure( CredentialsManagerException( - String.format( - "This device is not compatible with the %s class.", - SecureCredentialsManager::class.java.simpleName - ), e + CredentialsManagerException.Code.INCOMPATIBLE_DEVICE, + e ) ) return@execute @@ -450,8 +443,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT clearCredentials() callback.onFailure( CredentialsManagerException( - "A change on the Lock Screen security settings have deemed the encryption keys invalid and have been recreated. " + - "Any previously stored content is now lost. Please try saving the credentials again.", + CredentialsManagerException.Code.CRYPTO_EXCEPTION, e ) ) @@ -474,7 +466,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT val hasEmptyCredentials = TextUtils.isEmpty(credentials.accessToken) && TextUtils.isEmpty(credentials.idToken) if (hasEmptyCredentials) { - callback.onFailure(CredentialsManagerException("No Credentials were previously set.")) + callback.onFailure(CredentialsManagerException.NO_CREDENTIALS) return@execute } val willAccessTokenExpire = willExpire(expiresAt, minTtl.toLong()) @@ -485,7 +477,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT } if (credentials.refreshToken == null) { // ideally we will have to change the log to say, token expired or token needs to be refreshed but no refresh token exists to do so. - callback.onFailure(CredentialsManagerException("No Credentials were previously set.")) + callback.onFailure(CredentialsManagerException.NO_CREDENTIALS) return@execute } Log.d(TAG, "Credentials have expired. Renewing them now...") @@ -508,16 +500,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT val expiresAt = fresh.expiresAt.time val willAccessTokenExpire = willExpire(expiresAt, minTtl.toLong()) if (willAccessTokenExpire) { - val tokenLifetime = (expiresAt - currentTimeInMillis - minTtl * 1000) / -1000 - val wrongTtlException = CredentialsManagerException( - String.format( - Locale.getDefault(), - "The lifetime of the renewed Access Token (%d) is less than the minTTL requested (%d). Increase the 'Token Expiration' setting of your Auth0 API in the dashboard, or request a lower minTTL.", - tokenLifetime, - minTtl - ) - ) - callback.onFailure(wrongTtlException) + callback.onFailure(CredentialsManagerException.LARGE_MIN_TTL) return@execute } @@ -535,7 +518,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT } catch (error: Auth0Exception) { callback.onFailure( CredentialsManagerException( - "An error occurred while trying to use the Refresh Token to renew the Credentials.", + CredentialsManagerException.Code.RENEW_FAILED, error ) ) @@ -547,7 +530,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT callback.onSuccess(freshCredentials) } catch (error: CredentialsManagerException) { val exception = CredentialsManagerException( - "An error occurred while saving the refreshed Credentials.", error + CredentialsManagerException.Code.STORE_FAILED, error ) if (error.cause is IncompatibleDeviceException || error.cause is CryptoException) { exception.refreshedCredentials = freshCredentials diff --git a/sample/build.gradle b/sample/build.gradle index 5b3e0a66..ca9549c2 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -45,6 +45,9 @@ android { dependencies { implementation project(':auth0') + + def biometricLibraryVersion = "1.1.0" + implementation "androidx.biometric:biometric:$biometricLibraryVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.6.0' implementation 'androidx.appcompat:appcompat:1.3.0' From d846ecc38a8a2e42bc0568a651fba1dc7e54f3a5 Mon Sep 17 00:00:00 2001 From: Sai Venkat Desu Date: Mon, 10 Jun 2024 17:52:56 +0530 Subject: [PATCH 04/30] chore: minor improvements Signed-off-by: Sai Venkat Desu --- auth0/build.gradle | 2 +- .../storage/CredentialsManagerException.kt | 6 +++++ .../storage/SecureCredentialsManager.kt | 22 ++++++++++++------- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/auth0/build.gradle b/auth0/build.gradle index cafd4066..39c0d69b 100644 --- a/auth0/build.gradle +++ b/auth0/build.gradle @@ -76,6 +76,7 @@ ext { okhttpVersion = '4.12.0' powermockVersion = '2.0.9' coroutinesVersion = '1.6.2' + biometricLibraryVersion = '1.1.0' } @@ -91,7 +92,6 @@ dependencies { implementation 'com.google.code.gson:gson:2.8.9' implementation 'com.google.androidbrowserhelper:androidbrowserhelper:2.4.0' - def biometricLibraryVersion = "1.1.0" compileOnly "androidx.biometric:biometric:$biometricLibraryVersion" testImplementation 'junit:junit:4.13.2' diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManagerException.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManagerException.kt index df664176..74d9bced 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManagerException.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManagerException.kt @@ -114,6 +114,12 @@ public class CredentialsManagerException : } } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is CredentialsManagerException) return false + return code == other.code + } + /** * Returns true when this Android device doesn't support the cryptographic algorithms used * to handle encryption and decryption, false otherwise. diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt index f476195c..ffa18f2e 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt @@ -91,7 +91,6 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT * to use on the next call. We clear any existing credentials so #hasValidCredentials returns * a true value. Retrying this operation will succeed. */ - // suspect this is not something we do in Auth0.swift clearCredentials() throw CredentialsManagerException(CredentialsManagerException.Code.CRYPTO_EXCEPTION, e) } @@ -438,7 +437,6 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT ) return@execute } catch (e: CryptoException) { - // suspect this is not something we do in Auth0.swift //If keys were invalidated, existing credentials will not be recoverable. clearCredentials() callback.onFailure( @@ -476,8 +474,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT return@execute } if (credentials.refreshToken == null) { - // ideally we will have to change the log to say, token expired or token needs to be refreshed but no refresh token exists to do so. - callback.onFailure(CredentialsManagerException.NO_CREDENTIALS) + callback.onFailure(CredentialsManagerException.NO_REFRESH_TOKEN) return@execute } Log.d(TAG, "Credentials have expired. Renewing them now...") @@ -500,7 +497,17 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT val expiresAt = fresh.expiresAt.time val willAccessTokenExpire = willExpire(expiresAt, minTtl.toLong()) if (willAccessTokenExpire) { - callback.onFailure(CredentialsManagerException.LARGE_MIN_TTL) + val tokenLifetime = (expiresAt - currentTimeInMillis - minTtl * 1000) / -1000 + val wrongTtlException = CredentialsManagerException( + CredentialsManagerException.Code.LARGE_MIN_TTL, + String.format( + Locale.getDefault(), + "The lifetime of the renewed Access Token (%d) is less than the minTTL requested (%d). Increase the 'Token Expiration' setting of your Auth0 API in the dashboard, or request a lower minTTL.", + tokenLifetime, + minTtl + ) + ) + callback.onFailure(wrongTtlException) return@execute } @@ -542,11 +549,10 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT private fun isBiometricManagerPackageAvailable(): Boolean { return try { - // Attempt to load a class from the androidx.biometric package Class.forName("androidx.biometric.BiometricManager") - true // If successful, package is available + true } catch (e: ClassNotFoundException) { - false // If ClassNotFoundException is thrown, package is not available + false } } From ed374532d9771a7bd0d8abcaca21b48114146e9a Mon Sep 17 00:00:00 2001 From: Sai Venkat Desu Date: Mon, 10 Jun 2024 18:35:51 +0530 Subject: [PATCH 05/30] feat: updated sample app to use the getCredentialsWithAuthentication API Signed-off-by: Sai Venkat Desu --- .../com/auth0/sample/DatabaseLoginFragment.kt | 98 +++++++++++++------ 1 file changed, 70 insertions(+), 28 deletions(-) diff --git a/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt b/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt index 33850819..a07729b3 100644 --- a/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt +++ b/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt @@ -123,11 +123,12 @@ class DatabaseLoginFragment : Fragment() { private suspend fun dbLoginAsync(email: String, password: String) { try { - val result = authenticationApiClient.login(email, password, "Username-Password-Authentication") - .validateClaims() - .addParameter("scope", scope) - .addParameter("audience", audience) - .await() + val result = + authenticationApiClient.login(email, password, "Username-Password-Authentication") + .validateClaims() + .addParameter("scope", scope) + .addParameter("audience", audience) + .await() credentialsManager.saveCredentials(result) Snackbar.make( requireView(), @@ -199,7 +200,7 @@ class DatabaseLoginFragment : Fragment() { "Hello ${credentials.user.name}", Snackbar.LENGTH_LONG ).show() - } catch(error: AuthenticationException) { + } catch (error: AuthenticationException) { val message = if (error.isCanceled) "Browser was closed" else error.getDescription() Snackbar.make(requireView(), message, Snackbar.LENGTH_LONG).show() @@ -237,7 +238,7 @@ class DatabaseLoginFragment : Fragment() { "Logged out", Snackbar.LENGTH_LONG ).show() - } catch(error: AuthenticationException) { + } catch (error: AuthenticationException) { val message = if (error.isCanceled) "Browser was closed" else error.getDescription() Snackbar.make(requireView(), message, Snackbar.LENGTH_LONG).show() @@ -249,20 +250,47 @@ class DatabaseLoginFragment : Fragment() { } private fun getCreds() { - val localAuthenticationOptions = LocalAuthenticationOptions.Builder().title("Biometric").description("description").authenticator(AuthenticationLevel.STRONG).negativeButtonText("Cancel").build() - credentialsManager.getCredentialsWithAuthentication(requireActivity(), localAuthenticationOptions, null, 300, emptyMap(), emptyMap(), false, object : Callback { - override fun onSuccess(result: Credentials) { - Snackbar.make( - requireView(), - "Got credentials - ${result.accessToken}", - Snackbar.LENGTH_LONG - ).show() - } + val localAuthenticationOptions = + LocalAuthenticationOptions.Builder().title("Biometric").description("description") + .authenticator(AuthenticationLevel.STRONG).negativeButtonText("Cancel") + .build() + credentialsManager.getCredentialsWithAuthentication( + requireActivity(), + localAuthenticationOptions, + null, + 300, + emptyMap(), + emptyMap(), + false, + object : Callback { + override fun onSuccess(result: Credentials) { + Snackbar.make( + requireView(), + "Got credentials - ${result.accessToken}", + Snackbar.LENGTH_LONG + ).show() + } - override fun onFailure(error: CredentialsManagerException) { - Snackbar.make(requireView(), "${error.message}", Snackbar.LENGTH_LONG).show() - } - } ) + override fun onFailure(error: CredentialsManagerException) { + Snackbar.make(requireView(), "${error.message}", Snackbar.LENGTH_LONG).show() + + when (error) { + CredentialsManagerException.NO_CREDENTIALS -> { + // handle no credentials scenario + println("NO_CREDENTIALS: $error") + } + CredentialsManagerException.NO_REFRESH_TOKEN -> { + // handle no refresh token scenario + println("NO_REFRESH_TOKEN: $error") + } + CredentialsManagerException.STORE_FAILED -> { + // handle store failed scenario + println("STORE_FAILED: $error") + } + // ... similarly for other error codes + } + } + }) } private suspend fun getCredsAsync() { @@ -279,14 +307,20 @@ class DatabaseLoginFragment : Fragment() { } private fun getProfile() { - credentialsManager.getCredentials(object : Callback { + credentialsManager.getCredentials(object : + Callback { override fun onSuccess(result: Credentials) { val users = UsersAPIClient(account, result.accessToken) users.getProfile(result.user.getId()!!) - .start(object: Callback { + .start(object : Callback { override fun onFailure(error: ManagementException) { - Snackbar.make(requireView(), error.getDescription(), Snackbar.LENGTH_LONG).show() + Snackbar.make( + requireView(), + error.getDescription(), + Snackbar.LENGTH_LONG + ).show() } + override fun onSuccess(result: UserProfile) { Snackbar.make( requireView(), @@ -294,8 +328,9 @@ class DatabaseLoginFragment : Fragment() { Snackbar.LENGTH_LONG ).show() } - }) + }) } + override fun onFailure(error: CredentialsManagerException) { Snackbar.make(requireView(), "${error.message}", Snackbar.LENGTH_LONG).show() } @@ -324,14 +359,20 @@ class DatabaseLoginFragment : Fragment() { "random" to (0..100).random(), ) - credentialsManager.getCredentials(object : Callback { + credentialsManager.getCredentials(object : + Callback { override fun onSuccess(result: Credentials) { val users = UsersAPIClient(account, result.accessToken) users.updateMetadata(result.user.getId()!!, metadata) - .start(object: Callback { + .start(object : Callback { override fun onFailure(error: ManagementException) { - Snackbar.make(requireView(), error.getDescription(), Snackbar.LENGTH_LONG).show() + Snackbar.make( + requireView(), + error.getDescription(), + Snackbar.LENGTH_LONG + ).show() } + override fun onSuccess(result: UserProfile) { Snackbar.make( requireView(), @@ -339,8 +380,9 @@ class DatabaseLoginFragment : Fragment() { Snackbar.LENGTH_LONG ).show() } - }) + }) } + override fun onFailure(error: CredentialsManagerException) { Snackbar.make(requireView(), "${error.message}", Snackbar.LENGTH_LONG).show() } From fcc70294079c4d32f4b41d032e25a5082878b363 Mon Sep 17 00:00:00 2001 From: Sai Venkat Desu Date: Fri, 5 Jul 2024 12:41:39 +0530 Subject: [PATCH 06/30] chore: included biometric package directly instead of compileOnly Signed-off-by: Sai Venkat Desu --- auth0/build.gradle | 2 +- .../storage/LocalAuthenticationManager.kt | 3 +-- .../storage/SecureCredentialsManager.kt | 14 -------------- 3 files changed, 2 insertions(+), 17 deletions(-) diff --git a/auth0/build.gradle b/auth0/build.gradle index 39c0d69b..25083e5f 100644 --- a/auth0/build.gradle +++ b/auth0/build.gradle @@ -92,7 +92,7 @@ dependencies { implementation 'com.google.code.gson:gson:2.8.9' implementation 'com.google.androidbrowserhelper:androidbrowserhelper:2.4.0' - compileOnly "androidx.biometric:biometric:$biometricLibraryVersion" + implementation "androidx.biometric:biometric:$biometricLibraryVersion" testImplementation 'junit:junit:4.13.2' testImplementation 'org.hamcrest:java-hamcrest:2.0.0.0' diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationManager.kt index 8f64578f..39f695de 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationManager.kt @@ -11,9 +11,8 @@ internal class LocalAuthenticationManager( private val activity: FragmentActivity, private val authenticationOptions: LocalAuthenticationOptions, private val executor: Executor, + private val biometricManager: BiometricManager = BiometricManager.from(activity), ) { - private val biometricManager = BiometricManager.from(activity) - fun authenticate(resultCallback: Callback) { val authenticationLevels = if (authenticationOptions.enableDeviceCredentialFallback) { authenticationOptions.authenticationLevel.value or AuthenticationLevel.DEVICE_CREDENTIAL.value diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt index ffa18f2e..d50ab6b5 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt @@ -340,11 +340,6 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT callback: Callback ) { - if (!isBiometricManagerPackageAvailable()) { - callback.onFailure(CredentialsManagerException.BIOMETRICS_PACKAGE_NOT_FOUND) - return - } - val localAuthenticationManager = LocalAuthenticationManager( activity, authenticationOptions, @@ -547,15 +542,6 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT } } - private fun isBiometricManagerPackageAvailable(): Boolean { - return try { - Class.forName("androidx.biometric.BiometricManager") - true - } catch (e: ClassNotFoundException) { - false - } - } - internal companion object { private val TAG = SecureCredentialsManager::class.java.simpleName private const val KEY_CREDENTIALS = "com.auth0.credentials" From 424417e78754c1e55771813f11583bc541f1c040 Mon Sep 17 00:00:00 2001 From: Sai Venkat Desu Date: Mon, 8 Jul 2024 18:39:51 +0530 Subject: [PATCH 07/30] chore: broke down BaseCredentialManager into different abstract classes for Secured & Regular access to credentials Signed-off-by: Sai Venkat Desu --- .../storage/BaseCredentialsManager.kt | 33 +++++++++++++++---- .../storage/CredentialsManager.kt | 2 +- .../storage/SecureCredentialsManager.kt | 3 +- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt index f3169abe..4da6f634 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt @@ -1,6 +1,7 @@ package com.auth0.android.authentication.storage import androidx.annotation.VisibleForTesting +import androidx.fragment.app.FragmentActivity import com.auth0.android.authentication.AuthenticationAPIClient import com.auth0.android.callback.Callback import com.auth0.android.result.Credentials @@ -32,12 +33,6 @@ public abstract class BaseCredentialsManager internal constructor( @Throws(CredentialsManagerException::class) public abstract fun saveCredentials(credentials: Credentials) - public abstract fun getCredentials(callback: Callback) - public abstract fun getCredentials( - scope: String?, - minTtl: Int, - callback: Callback - ) public abstract fun clearCredentials() public abstract fun hasValidCredentials(): Boolean @@ -90,4 +85,30 @@ public abstract class BaseCredentialsManager internal constructor( protected fun hasExpired(expiresAt: Long): Boolean { return expiresAt <= currentTimeInMillis } +} + +public abstract class DefaultCredentialsManager internal constructor( + authenticationClient: AuthenticationAPIClient, + storage: Storage, + jwtDecoder: JWTDecoder +) : BaseCredentialsManager( + authenticationClient, storage, jwtDecoder +) { + public abstract fun getCredentials(callback: Callback) + public abstract fun getCredentials( + scope: String?, + minTtl: Int, + callback: Callback + ) +} + +public abstract class SecuredCredentialsManager internal constructor( + authenticationClient: AuthenticationAPIClient, + storage: Storage, + jwtDecoder: JWTDecoder +) : BaseCredentialsManager( + authenticationClient, storage, jwtDecoder +) { + public abstract fun getCredentials(fragmentActivity: FragmentActivity, authenticationOptions: LocalAuthenticationOptions, callback: Callback) + public abstract fun getCredentials(fragmentActivity: FragmentActivity, authenticationOptions: LocalAuthenticationOptions, scope: String?, minTtl: Int, callback: Callback) } \ No newline at end of file diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt index 7f5a20b6..197c7bfb 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt @@ -21,7 +21,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting storage: Storage, jwtDecoder: JWTDecoder, private val serialExecutor: Executor -) : BaseCredentialsManager(authenticationClient, storage, jwtDecoder) { +) : DefaultCredentialsManager(authenticationClient, storage, jwtDecoder) { /** * Creates a new instance of the manager that will store the credentials in the given Storage. * diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt index d50ab6b5..43e6731d 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt @@ -30,8 +30,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT storage: Storage, private val crypto: CryptoUtil, jwtDecoder: JWTDecoder, - private val serialExecutor: Executor -) : BaseCredentialsManager(apiClient, storage, jwtDecoder) { +) : SecuredCredentialsManager(apiClient, storage, jwtDecoder) { private val gson: Gson = GsonProvider.gson From 156d47263c1179e5ea627232d261f891e9f5cd78 Mon Sep 17 00:00:00 2001 From: Sai Venkat Desu Date: Mon, 8 Jul 2024 18:43:28 +0530 Subject: [PATCH 08/30] chore: handled unsupported combinations of authentication levels on API levels 28, 29 and less than 30 Signed-off-by: Sai Venkat Desu --- .../storage/LocalAuthenticationManager.kt | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationManager.kt index 39f695de..2bdfc9e2 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationManager.kt @@ -1,5 +1,9 @@ package com.auth0.android.authentication.storage +import android.os.Build +import android.os.Handler +import android.os.Looper +import androidx.annotation.VisibleForTesting import androidx.biometric.BiometricManager import androidx.biometric.BiometricPrompt import androidx.fragment.app.FragmentActivity @@ -12,8 +16,24 @@ internal class LocalAuthenticationManager( private val authenticationOptions: LocalAuthenticationOptions, private val executor: Executor, private val biometricManager: BiometricManager = BiometricManager.from(activity), -) { - fun authenticate(resultCallback: Callback) { + private val uiThreadExecutor = UiThreadExecutor() + + fun authenticate() { + // On Android API 29 and below, specifying DEVICE_CREDENTIAL alone as the authentication level is not supported. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R && authenticationOptions.authenticationLevel.value == AuthenticationLevel.DEVICE_CREDENTIAL.value) { + resultCallback.onFailure(CredentialsManagerException.BIOMETRIC_ERROR_DEVICE_CREDENTIAL_NOT_AVAILABLE) + return + } + + // On Android API 28 and 29, specifying BIOMETRIC_STRONG as the authentication level along with enabling device credential fallback is not supported. + if (Build.VERSION.SDK_INT in Build.VERSION_CODES.P..Build.VERSION_CODES.Q && + authenticationOptions.authenticationLevel.value == AuthenticationLevel.STRONG.value && + authenticationOptions.enableDeviceCredentialFallback + ) { + resultCallback.onFailure(CredentialsManagerException.BIOMETRIC_ERROR_STRONG_AND_DEVICE_CREDENTIAL_NOT_AVAILABLE) + return + } + val authenticationLevels = if (authenticationOptions.enableDeviceCredentialFallback) { authenticationOptions.authenticationLevel.value or AuthenticationLevel.DEVICE_CREDENTIAL.value } else { @@ -36,7 +56,7 @@ internal class LocalAuthenticationManager( setTitle(title) setSubtitle(subtitle) setDescription(description) - if (!enableDeviceCredentialFallback) { + if (!enableDeviceCredentialFallback && authenticationLevel != AuthenticationLevel.DEVICE_CREDENTIAL) { setNegativeButtonText(negativeButtonText) } } From ddd9b5e8991579b3d2d5975e81bf144c2314f733 Mon Sep 17 00:00:00 2001 From: Sai Venkat Desu Date: Mon, 8 Jul 2024 18:44:25 +0530 Subject: [PATCH 09/30] chore: updated LocalAuthenticationManager to implement AuthenticationCallback for better testability Signed-off-by: Sai Venkat Desu --- .../storage/CredentialsManagerException.kt | 93 ++++++++++++++++--- .../storage/LocalAuthenticationManager.kt | 73 +++++++++------ 2 files changed, 123 insertions(+), 43 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManagerException.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManagerException.kt index 74d9bced..ae6c65dd 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManagerException.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManagerException.kt @@ -15,19 +15,33 @@ public class CredentialsManagerException : NO_REFRESH_TOKEN, RENEW_FAILED, STORE_FAILED, - BIOMETRICS_FAILED, REVOKE_FAILED, LARGE_MIN_TTL, INCOMPATIBLE_DEVICE, CRYPTO_EXCEPTION, - BIOMETRICS_PACKAGE_NOT_FOUND, - BIOMETRIC_STATUS_UNKNOWN, - BIOMETRIC_AUTHENTICATION_CHECK_FAILED, + BIOMETRIC_ERROR_STATUS_UNKNOWN, BIOMETRIC_ERROR_UNSUPPORTED, BIOMETRIC_ERROR_HW_UNAVAILABLE, BIOMETRIC_ERROR_NONE_ENROLLED, BIOMETRIC_ERROR_NO_HARDWARE, BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED, + BIOMETRIC_ERROR_DEVICE_CREDENTIAL_NOT_AVAILABLE, + BIOMETRIC_ERROR_STRONG_AND_DEVICE_CREDENTIAL_NOT_AVAILABLE, + BIOMETRIC_AUTHENTICATION_CHECK_FAILED, + BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL, + BIOMETRIC_ERROR_NEGATIVE_BUTTON, + BIOMETRIC_ERROR_HW_NOT_PRESENT, + BIOMETRIC_ERROR_NO_BIOMETRICS, + BIOMETRIC_ERROR_USER_CANCELED, + BIOMETRIC_ERROR_LOCKOUT_PERMANENT, + BIOMETRIC_ERROR_VENDOR, + BIOMETRIC_ERROR_LOCKOUT, + BIOMETRIC_ERROR_CANCELED, + BIOMETRIC_ERROR_NO_SPACE, + BIOMETRIC_ERROR_TIMEOUT, + BIOMETRIC_ERROR_UNABLE_TO_PROCESS, + BIOMETRICS_INVALID_USER, + BIOMETRIC_AUTHENTICATION_FAILED, } private var code: Code? @@ -58,8 +72,6 @@ public class CredentialsManagerException : CredentialsManagerException(Code.RENEW_FAILED) public val STORE_FAILED: CredentialsManagerException = CredentialsManagerException(Code.STORE_FAILED) - public val BIOMETRICS_FAILED: CredentialsManagerException = - CredentialsManagerException(Code.BIOMETRICS_FAILED) public val REVOKE_FAILED: CredentialsManagerException = CredentialsManagerException(Code.REVOKE_FAILED) public val LARGE_MIN_TTL: CredentialsManagerException = @@ -68,10 +80,10 @@ public class CredentialsManagerException : CredentialsManagerException(Code.INCOMPATIBLE_DEVICE) public val CRYPTO_EXCEPTION: CredentialsManagerException = CredentialsManagerException(Code.CRYPTO_EXCEPTION) - public val BIOMETRICS_PACKAGE_NOT_FOUND: CredentialsManagerException = - CredentialsManagerException(Code.BIOMETRICS_PACKAGE_NOT_FOUND) - public val BIOMETRIC_STATUS_UNKNOWN: CredentialsManagerException = - CredentialsManagerException(Code.BIOMETRIC_STATUS_UNKNOWN) + + // Exceptions thrown when trying to check authentication is possible or not + public val BIOMETRIC_ERROR_STATUS_UNKNOWN: CredentialsManagerException = + CredentialsManagerException(Code.BIOMETRIC_ERROR_STATUS_UNKNOWN) public val BIOMETRIC_ERROR_UNSUPPORTED: CredentialsManagerException = CredentialsManagerException(Code.BIOMETRIC_ERROR_UNSUPPORTED) public val BIOMETRIC_ERROR_HW_UNAVAILABLE: CredentialsManagerException = @@ -82,10 +94,45 @@ public class CredentialsManagerException : CredentialsManagerException(Code.BIOMETRIC_ERROR_NO_HARDWARE) public val BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED: CredentialsManagerException = CredentialsManagerException(Code.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED) + public val BIOMETRIC_ERROR_DEVICE_CREDENTIAL_NOT_AVAILABLE: CredentialsManagerException = + CredentialsManagerException(Code.BIOMETRIC_ERROR_DEVICE_CREDENTIAL_NOT_AVAILABLE) + public val BIOMETRIC_ERROR_STRONG_AND_DEVICE_CREDENTIAL_NOT_AVAILABLE: CredentialsManagerException = + CredentialsManagerException(Code.BIOMETRIC_ERROR_STRONG_AND_DEVICE_CREDENTIAL_NOT_AVAILABLE) public val BIOMETRIC_AUTHENTICATION_CHECK_FAILED: CredentialsManagerException = CredentialsManagerException(Code.BIOMETRIC_AUTHENTICATION_CHECK_FAILED) + // Exceptions thrown when trying to authenticate with biometrics + public val BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL: CredentialsManagerException = + CredentialsManagerException(Code.BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL) + public val BIOMETRIC_ERROR_NEGATIVE_BUTTON: CredentialsManagerException = + CredentialsManagerException(Code.BIOMETRIC_ERROR_NEGATIVE_BUTTON) + public val BIOMETRIC_ERROR_HW_NOT_PRESENT: CredentialsManagerException = + CredentialsManagerException(Code.BIOMETRIC_ERROR_HW_NOT_PRESENT) + public val BIOMETRIC_ERROR_NO_BIOMETRICS: CredentialsManagerException = + CredentialsManagerException(Code.BIOMETRIC_ERROR_NO_BIOMETRICS) + public val BIOMETRIC_ERROR_USER_CANCELED: CredentialsManagerException = + CredentialsManagerException(Code.BIOMETRIC_ERROR_USER_CANCELED) + public val BIOMETRIC_ERROR_LOCKOUT_PERMANENT: CredentialsManagerException = + CredentialsManagerException(Code.BIOMETRIC_ERROR_LOCKOUT_PERMANENT) + public val BIOMETRIC_ERROR_VENDOR: CredentialsManagerException = + CredentialsManagerException(Code.BIOMETRIC_ERROR_VENDOR) + public val BIOMETRIC_ERROR_LOCKOUT: CredentialsManagerException = + CredentialsManagerException(Code.BIOMETRIC_ERROR_LOCKOUT) + public val BIOMETRIC_ERROR_CANCELED: CredentialsManagerException = + CredentialsManagerException(Code.BIOMETRIC_ERROR_CANCELED) + public val BIOMETRIC_ERROR_NO_SPACE: CredentialsManagerException = + CredentialsManagerException(Code.BIOMETRIC_ERROR_NO_SPACE) + public val BIOMETRIC_ERROR_TIMEOUT: CredentialsManagerException = + CredentialsManagerException(Code.BIOMETRIC_ERROR_TIMEOUT) + public val BIOMETRIC_ERROR_UNABLE_TO_PROCESS: CredentialsManagerException = + CredentialsManagerException(Code.BIOMETRIC_ERROR_UNABLE_TO_PROCESS) + public val BIOMETRIC_AUTHENTICATION_FAILED: CredentialsManagerException = + CredentialsManagerException(Code.BIOMETRIC_AUTHENTICATION_FAILED) + public val BIOMETRICS_INVALID_USER: CredentialsManagerException = + CredentialsManagerException(Code.BIOMETRICS_INVALID_USER) + + private fun getMessage(code: Code): String { return when (code) { Code.INVALID_CREDENTIALS -> "Credentials must have a valid access_token or id_token value." @@ -93,7 +140,6 @@ public class CredentialsManagerException : Code.NO_REFRESH_TOKEN -> "Credentials need to be renewed but no Refresh Token is available to renew them." Code.RENEW_FAILED -> "An error occurred while trying to use the Refresh Token to renew the Credentials." Code.STORE_FAILED -> "An error occurred while saving the refreshed Credentials." - Code.BIOMETRICS_FAILED -> "The user didn't pass the authentication challenge." Code.REVOKE_FAILED -> "The revocation of the refresh token failed." Code.LARGE_MIN_TTL -> "The minTTL requested is greater than the lifetime of the renewed access token. Request a lower minTTL or increase the 'Token Expiration' value in the settings page of your Auth0 API." Code.INCOMPATIBLE_DEVICE -> String.format( @@ -101,15 +147,32 @@ public class CredentialsManagerException : SecureCredentialsManager::class.java.simpleName ) - Code.CRYPTO_EXCEPTION -> "A change on the Lock Screen security settings have deemed the encryption keys invalid and have been recreated. Please try saving the credentials again." - Code.BIOMETRICS_PACKAGE_NOT_FOUND -> "Package androidx.biometric:biometric is not found. Please add it to your dependencies to enable authentication before retrieving credentials." - Code.BIOMETRIC_STATUS_UNKNOWN -> "Unable to determine whether the user can authenticate." + Code.CRYPTO_EXCEPTION -> "A change on the Lock Screen security settings have deemed the encryption keys invalid and have been recreated. Any previously stored content is now lost. Please try saving the credentials again." + + Code.BIOMETRIC_ERROR_STATUS_UNKNOWN -> "Unable to determine whether the user can authenticate." Code.BIOMETRIC_ERROR_UNSUPPORTED -> "Cannot authenticate because the specified options are incompatible with the current Android version." Code.BIOMETRIC_ERROR_HW_UNAVAILABLE -> "Cannot authenticate because the hardware is unavailable. Try again later." Code.BIOMETRIC_ERROR_NONE_ENROLLED -> "Cannot authenticate because no biometric or device credential is enrolled for the user." Code.BIOMETRIC_ERROR_NO_HARDWARE -> "Cannot authenticate because there is no suitable hardware (e.g. no biometric sensor or no keyguard)." Code.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> "Cannot authenticate because a security vulnerability has been discovered with one or more hardware sensors. The affected sensor(s) are unavailable until a security update has addressed the issue." - Code.BIOMETRIC_AUTHENTICATION_CHECK_FAILED -> "Failed to determine if the user can authenticate with an authenticator that meets the given requirements" + Code.BIOMETRIC_AUTHENTICATION_CHECK_FAILED -> "Cannot authenticate as failed to determine if the user can authenticate with an authenticator that meets the given requirements." + Code.BIOMETRIC_ERROR_DEVICE_CREDENTIAL_NOT_AVAILABLE -> "Cannot authenticate as DEVICE_CREDENTIAL alone as a authentication level is not supported on Android API Level less than 30" + Code.BIOMETRIC_ERROR_STRONG_AND_DEVICE_CREDENTIAL_NOT_AVAILABLE -> "Cannot authenticate as BIOMETRIC_STRONG authentication level along with device credential fallback being enabled is not supported on Android API Levels 28 & 29" + + Code.BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL -> "Failed to authenticate because the device does not have pin, pattern, or password setup." + Code.BIOMETRIC_ERROR_NEGATIVE_BUTTON -> "Failed to authenticate as the user pressed the negative button." + Code.BIOMETRIC_ERROR_HW_NOT_PRESENT -> "Failed to authenticate because the device does not have the required authentication hardware." + Code.BIOMETRIC_ERROR_NO_BIOMETRICS -> "Failed to authenticate because the user does not have any biometrics enrolled." + Code.BIOMETRIC_ERROR_USER_CANCELED -> "Failed to authenticate because the user canceled the operation." + Code.BIOMETRIC_ERROR_LOCKOUT_PERMANENT -> "Failed to authenticate because the user has been permanently locked out." + Code.BIOMETRIC_ERROR_VENDOR -> "Failed to authenticate because of a vendor-specific error." + Code.BIOMETRIC_ERROR_LOCKOUT -> "Failed to authenticate because the user has been temporarily locked out, this occurs after 5 failed attempts and lasts for 30 seconds." + Code.BIOMETRIC_ERROR_CANCELED -> "Failed to authenticate because the operation was canceled as the biometric sensor is unavailable, this may happen when the user is switched, the device is locked." + Code.BIOMETRIC_ERROR_NO_SPACE -> "Failed to authenticate because there is not enough storage remaining on the device." + Code.BIOMETRIC_ERROR_TIMEOUT -> "Failed to authenticate because the operation timed out." + Code.BIOMETRIC_ERROR_UNABLE_TO_PROCESS -> "Failed to authenticate because the sensor was unable to process the current image." + Code.BIOMETRICS_INVALID_USER -> "The user didn't pass the authentication challenge." + Code.BIOMETRIC_AUTHENTICATION_FAILED -> "Biometric authentication failed." } } } diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationManager.kt index 2bdfc9e2..3b7e8196 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationManager.kt @@ -6,6 +6,7 @@ import android.os.Looper import androidx.annotation.VisibleForTesting import androidx.biometric.BiometricManager import androidx.biometric.BiometricPrompt +import androidx.biometric.BiometricPrompt.AuthenticationCallback import androidx.fragment.app.FragmentActivity import com.auth0.android.callback.Callback import java.util.concurrent.Executor @@ -14,8 +15,11 @@ import java.util.concurrent.Executor internal class LocalAuthenticationManager( private val activity: FragmentActivity, private val authenticationOptions: LocalAuthenticationOptions, - private val executor: Executor, private val biometricManager: BiometricManager = BiometricManager.from(activity), + @get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal val resultCallback: Callback, +) : AuthenticationCallback() { + private val uiThreadExecutor = UiThreadExecutor() fun authenticate() { @@ -66,12 +70,26 @@ internal class LocalAuthenticationManager( val biometricPromptInfo = bioMetricPromptInfoBuilder.build() val biometricPrompt = BiometricPrompt( activity, - executor, - biometricPromptAuthenticationCallback(resultCallback) + uiThreadExecutor, + this ) biometricPrompt.authenticate(biometricPromptInfo) } + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + resultCallback.onFailure(CredentialsManagerException.BIOMETRICS_INVALID_USER) + } + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + resultCallback.onSuccess(true) + } + + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + resultCallback.onFailure(generateExceptionFromAuthenticationError(errorCode)) + } private fun generateExceptionFromAuthenticationPossibilityError(errorCode: Int): CredentialsManagerException { val exceptionCode = mapOf( @@ -80,38 +98,37 @@ internal class LocalAuthenticationManager( BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE to CredentialsManagerException.BIOMETRIC_ERROR_NO_HARDWARE, BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED to CredentialsManagerException.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED, BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED to CredentialsManagerException.BIOMETRIC_ERROR_UNSUPPORTED, - BiometricManager.BIOMETRIC_STATUS_UNKNOWN to CredentialsManagerException.BIOMETRIC_STATUS_UNKNOWN + BiometricManager.BIOMETRIC_STATUS_UNKNOWN to CredentialsManagerException.BIOMETRIC_ERROR_STATUS_UNKNOWN ) return exceptionCode[errorCode] ?: CredentialsManagerException.BIOMETRIC_AUTHENTICATION_CHECK_FAILED } - private val biometricPromptAuthenticationCallback = - { callback: Callback -> - object : BiometricPrompt.AuthenticationCallback() { - override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { - super.onAuthenticationSucceeded(result) - callback.onSuccess(true) - } + private fun generateExceptionFromAuthenticationError(errorCode: Int): CredentialsManagerException { + val exceptionCode = mapOf( + BiometricPrompt.ERROR_HW_UNAVAILABLE to CredentialsManagerException.BIOMETRIC_ERROR_HW_UNAVAILABLE, + BiometricPrompt.ERROR_UNABLE_TO_PROCESS to CredentialsManagerException.BIOMETRIC_ERROR_UNABLE_TO_PROCESS, + BiometricPrompt.ERROR_TIMEOUT to CredentialsManagerException.BIOMETRIC_ERROR_TIMEOUT, + BiometricPrompt.ERROR_NO_SPACE to CredentialsManagerException.BIOMETRIC_ERROR_NO_SPACE, + BiometricPrompt.ERROR_CANCELED to CredentialsManagerException.BIOMETRIC_ERROR_CANCELED, + BiometricPrompt.ERROR_LOCKOUT to CredentialsManagerException.BIOMETRIC_ERROR_LOCKOUT, + BiometricPrompt.ERROR_VENDOR to CredentialsManagerException.BIOMETRIC_ERROR_VENDOR, + BiometricPrompt.ERROR_LOCKOUT_PERMANENT to CredentialsManagerException.BIOMETRIC_ERROR_LOCKOUT_PERMANENT, + BiometricPrompt.ERROR_USER_CANCELED to CredentialsManagerException.BIOMETRIC_ERROR_USER_CANCELED, + BiometricPrompt.ERROR_NO_BIOMETRICS to CredentialsManagerException.BIOMETRIC_ERROR_NO_BIOMETRICS, + BiometricPrompt.ERROR_HW_NOT_PRESENT to CredentialsManagerException.BIOMETRIC_ERROR_HW_NOT_PRESENT, + BiometricPrompt.ERROR_NEGATIVE_BUTTON to CredentialsManagerException.BIOMETRIC_ERROR_NEGATIVE_BUTTON, + BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL to CredentialsManagerException.BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL, + ) + return exceptionCode[errorCode] + ?: CredentialsManagerException.BIOMETRIC_AUTHENTICATION_FAILED + } - override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { - super.onAuthenticationError(errorCode, errString) - callback.onFailure( - CredentialsManagerException( - CredentialsManagerException.Code.BIOMETRICS_FAILED, - "Biometrics Authentication Failed with error code $errorCode due to $errString" - ) - ) - } + class UiThreadExecutor : Executor { + private val handler = Handler(Looper.getMainLooper()) - override fun onAuthenticationFailed() { - super.onAuthenticationFailed() - callback.onFailure(CredentialsManagerException.BIOMETRICS_FAILED) - } - } + override fun execute(command: Runnable) { + handler.post(command) } - - internal companion object { - private val TAG = LocalAuthenticationManager::class.java.simpleName } } \ No newline at end of file From bd30ba259d3724f5d14d8f7dd8a1ff6ea2105378 Mon Sep 17 00:00:00 2001 From: Sai Venkat Desu Date: Mon, 8 Jul 2024 18:46:10 +0530 Subject: [PATCH 10/30] chore: changed LocalAuthenticationManager creation to factory pattern for easier testability Signed-off-by: Sai Venkat Desu --- .../DefaultLocalAuthenticationManagerFactory.kt | 16 ++++++++++++++++ .../storage/LocalAuthenticationManagerFactory.kt | 12 ++++++++++++ .../storage/SecureCredentialsManager.kt | 5 ++++- 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 auth0/src/main/java/com/auth0/android/authentication/storage/DefaultLocalAuthenticationManagerFactory.kt create mode 100644 auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationManagerFactory.kt diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/DefaultLocalAuthenticationManagerFactory.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/DefaultLocalAuthenticationManagerFactory.kt new file mode 100644 index 00000000..6d2d299f --- /dev/null +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/DefaultLocalAuthenticationManagerFactory.kt @@ -0,0 +1,16 @@ +package com.auth0.android.authentication.storage + +import androidx.fragment.app.FragmentActivity +import com.auth0.android.callback.Callback + +internal class DefaultLocalAuthenticationManagerFactory : LocalAuthenticationManagerFactory { + override fun create( + activity: FragmentActivity, + authenticationOptions: LocalAuthenticationOptions, + resultCallback: Callback + ): LocalAuthenticationManager = LocalAuthenticationManager( + activity = activity, + authenticationOptions = authenticationOptions, + resultCallback = resultCallback + ) +} \ No newline at end of file diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationManagerFactory.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationManagerFactory.kt new file mode 100644 index 00000000..e20590ee --- /dev/null +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationManagerFactory.kt @@ -0,0 +1,12 @@ +package com.auth0.android.authentication.storage + +import androidx.fragment.app.FragmentActivity +import com.auth0.android.callback.Callback + +internal interface LocalAuthenticationManagerFactory { + fun create( + activity: FragmentActivity, + authenticationOptions: LocalAuthenticationOptions, + resultCallback: Callback + ): LocalAuthenticationManager +} \ No newline at end of file diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt index 43e6731d..413dfda7 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt @@ -30,6 +30,8 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT storage: Storage, private val crypto: CryptoUtil, jwtDecoder: JWTDecoder, + private val serialExecutor: Executor, + private val localAuthenticationManagerFactory: LocalAuthenticationManagerFactory ) : SecuredCredentialsManager(apiClient, storage, jwtDecoder) { private val gson: Gson = GsonProvider.gson @@ -50,7 +52,8 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT storage, CryptoUtil(context, storage, KEY_ALIAS), JWTDecoder(), - Executors.newSingleThreadExecutor() + Executors.newSingleThreadExecutor(), + DefaultLocalAuthenticationManagerFactory() ) From 1f949fd19c1d329abf15b7d586e2cf4beb4dcabb Mon Sep 17 00:00:00 2001 From: Sai Venkat Desu Date: Mon, 8 Jul 2024 18:47:53 +0530 Subject: [PATCH 11/30] chore: updated all the methods in the SecureCredentialsManager to contain fragmentActivity and authenticationOptions Signed-off-by: Sai Venkat Desu --- .../storage/SecureCredentialsManager.kt | 226 ++++++++++++------ 1 file changed, 154 insertions(+), 72 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt index 413dfda7..e435ae31 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt @@ -2,6 +2,8 @@ package com.auth0.android.authentication.storage import android.app.Activity import android.content.Context +import android.opengl.Visibility +import android.provider.Settings.Secure import android.text.TextUtils import android.util.Base64 import android.util.Log @@ -104,14 +106,19 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT * at all with the cryptographic implementation and will have [CredentialsManagerException.isDeviceIncompatible] return true. * This is a Coroutine that is exposed only for Kotlin. * - * If a LockScreen is setup and [SecureCredentialsManager.requireAuthentication] was called, the user will be asked to authenticate before accessing - * the credentials. Your activity must override the [Activity.onActivityResult] method and call - * [SecureCredentialsManager.checkAuthenticationResult] with the received values. + * If the user's lock screen authentication configuration matches the authentication level specified in the [authenticationOptions], + * the user will be prompted to authenticate before accessing the credentials. + * + * @param fragmentActivity the activity that will be used to host the [androidx.biometric.BiometricPrompt] + * @param authenticationOptions, an instance of [LocalAuthenticationOptions] used to configure authentication performed using [androidx.biometric.BiometricPrompt] */ @JvmSynthetic @Throws(CredentialsManagerException::class) - public suspend fun awaitCredentials(): Credentials { - return awaitCredentials(null, 0) + public suspend fun awaitCredentials( + fragmentActivity: FragmentActivity, + authenticationOptions: LocalAuthenticationOptions + ): Credentials { + return awaitCredentials(fragmentActivity, authenticationOptions, null, 0) } /** @@ -120,17 +127,23 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT * at all with the cryptographic implementation and will have [CredentialsManagerException.isDeviceIncompatible] return true. * This is a Coroutine that is exposed only for Kotlin. * - * If a LockScreen is setup and [SecureCredentialsManager.requireAuthentication] was called, the user will be asked to authenticate before accessing - * the credentials. Your activity must override the [Activity.onActivityResult] method and call - * [SecureCredentialsManager.checkAuthenticationResult] with the received values. + * If the user's lock screen authentication configuration matches the authentication level specified in the [authenticationOptions], + * the user will be prompted to authenticate before accessing the credentials. * + * @param fragmentActivity the activity that will be used to host the [androidx.biometric.BiometricPrompt] + * @param authenticationOptions, an instance of [LocalAuthenticationOptions] used to configure authentication performed using [androidx.biometric.BiometricPrompt] * @param scope the scope to request for the access token. If null is passed, the previous scope will be kept. * @param minTtl the minimum time in seconds that the access token should last before expiration. */ @JvmSynthetic @Throws(CredentialsManagerException::class) - public suspend fun awaitCredentials(scope: String?, minTtl: Int): Credentials { - return awaitCredentials(scope, minTtl, emptyMap()) + public suspend fun awaitCredentials( + fragmentActivity: FragmentActivity, + authenticationOptions: LocalAuthenticationOptions, + scope: String?, + minTtl: Int + ): Credentials { + return awaitCredentials(fragmentActivity, authenticationOptions, scope, minTtl, emptyMap()) } /** @@ -139,10 +152,11 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT * at all with the cryptographic implementation and will have [CredentialsManagerException.isDeviceIncompatible] return true. * This is a Coroutine that is exposed only for Kotlin. * - * If a LockScreen is setup and [SecureCredentialsManager.requireAuthentication] was called, the user will be asked to authenticate before accessing - * the credentials. Your activity must override the [Activity.onActivityResult] method and call - * [SecureCredentialsManager.checkAuthenticationResult] with the received values. + * If the user's lock screen authentication configuration matches the authentication level specified in the [authenticationOptions], + * the user will be prompted to authenticate before accessing the credentials. * + * @param fragmentActivity the activity that will be used to host the [androidx.biometric.BiometricPrompt] + * @param authenticationOptions, an instance of [LocalAuthenticationOptions] used to configure authentication performed using [androidx.biometric.BiometricPrompt] * @param scope the scope to request for the access token. If null is passed, the previous scope will be kept. * @param minTtl the minimum time in seconds that the access token should last before expiration. * @param parameters additional parameters to send in the request to refresh expired credentials @@ -150,11 +164,20 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT @JvmSynthetic @Throws(CredentialsManagerException::class) public suspend fun awaitCredentials( + fragmentActivity: FragmentActivity, + authenticationOptions: LocalAuthenticationOptions, scope: String?, minTtl: Int, parameters: Map ): Credentials { - return awaitCredentials(scope, minTtl, parameters, false) + return awaitCredentials( + fragmentActivity, + authenticationOptions, + scope, + minTtl, + parameters, + false + ) } /** @@ -163,10 +186,11 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT * at all with the cryptographic implementation and will have [CredentialsManagerException.isDeviceIncompatible] return true. * This is a Coroutine that is exposed only for Kotlin. * - * If a LockScreen is setup and [SecureCredentialsManager.requireAuthentication] was called, the user will be asked to authenticate before accessing - * the credentials. Your activity must override the [Activity.onActivityResult] method and call - * [SecureCredentialsManager.checkAuthenticationResult] with the received values. + * If the user's lock screen authentication configuration matches the authentication level specified in the [authenticationOptions], + * the user will be prompted to authenticate before accessing the credentials. * + * @param fragmentActivity the activity that will be used to host the [androidx.biometric.BiometricPrompt] + * @param authenticationOptions, an instance of [LocalAuthenticationOptions] used to configure authentication performed using [androidx.biometric.BiometricPrompt] * @param scope the scope to request for the access token. If null is passed, the previous scope will be kept. * @param minTtl the minimum time in seconds that the access token should last before expiration. * @param parameters additional parameters to send in the request to refresh expired credentials. @@ -175,12 +199,22 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT @JvmSynthetic @Throws(CredentialsManagerException::class) public suspend fun awaitCredentials( + fragmentActivity: FragmentActivity, + authenticationOptions: LocalAuthenticationOptions, scope: String?, minTtl: Int, parameters: Map, forceRefresh: Boolean, ): Credentials { - return awaitCredentials(scope, minTtl, parameters, mapOf(), forceRefresh) + return awaitCredentials( + fragmentActivity, + authenticationOptions, + scope, + minTtl, + parameters, + mapOf(), + forceRefresh + ) } /** @@ -189,10 +223,11 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT * at all with the cryptographic implementation and will have [CredentialsManagerException.isDeviceIncompatible] return true. * This is a Coroutine that is exposed only for Kotlin. * - * If a LockScreen is setup and [SecureCredentialsManager.requireAuthentication] was called, the user will be asked to authenticate before accessing - * the credentials. Your activity must override the [Activity.onActivityResult] method and call - * [SecureCredentialsManager.checkAuthenticationResult] with the received values. + * If the user's lock screen authentication configuration matches the authentication level specified in the [authenticationOptions], + * the user will be prompted to authenticate before accessing the credentials. * + * @param fragmentActivity the activity that will be used to host the [androidx.biometric.BiometricPrompt] + * @param authenticationOptions, an instance of [LocalAuthenticationOptions] used to configure authentication performed using [androidx.biometric.BiometricPrompt] * @param scope the scope to request for the access token. If null is passed, the previous scope will be kept. * @param minTtl the minimum time in seconds that the access token should last before expiration. * @param parameters additional parameters to send in the request to refresh expired credentials. @@ -202,6 +237,8 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT @JvmSynthetic @Throws(CredentialsManagerException::class) public suspend fun awaitCredentials( + fragmentActivity: FragmentActivity, + authenticationOptions: LocalAuthenticationOptions, scope: String?, minTtl: Int, parameters: Map, @@ -210,6 +247,8 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT ): Credentials { return suspendCancellableCoroutine { continuation -> getCredentials( + fragmentActivity, + authenticationOptions, scope, minTtl, parameters, @@ -232,15 +271,19 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT * If something unexpected happens, the [Callback.onFailure] method will be called with the error. Some devices are not compatible * at all with the cryptographic implementation and will have [CredentialsManagerException.isDeviceIncompatible] return true. * + * If the user's lock screen authentication configuration matches the authentication level specified in the [authenticationOptions], + * the user will be prompted to authenticate before accessing the credentials. * - * If a LockScreen is setup and [SecureCredentialsManager.requireAuthentication] was called, the user will be asked to authenticate before accessing - * the credentials. Your activity must override the [Activity.onActivityResult] method and call - * [checkAuthenticationResult] with the received values. - * + * @param fragmentActivity the activity that will be used to host the [androidx.biometric.BiometricPrompt] + * @param authenticationOptions an instance of [LocalAuthenticationOptions] used to configure authentication performed using [androidx.biometric.BiometricPrompt] * @param callback the callback to receive the result in. */ - override fun getCredentials(callback: Callback) { - getCredentials(null, 0, callback) + override fun getCredentials( + fragmentActivity: FragmentActivity, + authenticationOptions: LocalAuthenticationOptions, + callback: Callback + ) { + getCredentials(fragmentActivity, authenticationOptions, null, 0, callback) } /** @@ -248,21 +291,23 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT * If something unexpected happens, the [Callback.onFailure] method will be called with the error. Some devices are not compatible * at all with the cryptographic implementation and will have [CredentialsManagerException.isDeviceIncompatible] return true. * + * If the user's lock screen authentication configuration matches the authentication level specified in the [authenticationOptions], + * the user will be prompted to authenticate before accessing the credentials. * - * If a LockScreen is setup and [SecureCredentialsManager.requireAuthentication] was called, the user will be asked to authenticate before accessing - * the credentials. Your activity must override the [Activity.onActivityResult] method and call - * [SecureCredentialsManager.checkAuthenticationResult] with the received values. - * + * @param fragmentActivity the activity that will be used to host the [androidx.biometric.BiometricPrompt] + * @param authenticationOptions an instance of [LocalAuthenticationOptions] used to configure authentication performed using [androidx.biometric.BiometricPrompt] * @param scope the scope to request for the access token. If null is passed, the previous scope will be kept. * @param minTtl the minimum time in seconds that the access token should last before expiration. * @param callback the callback to receive the result in. */ override fun getCredentials( + fragmentActivity: FragmentActivity, + authenticationOptions: LocalAuthenticationOptions, scope: String?, minTtl: Int, callback: Callback ) { - getCredentials(scope, minTtl, emptyMap(), callback) + getCredentials(fragmentActivity, authenticationOptions, scope, minTtl, emptyMap(), callback) } /** @@ -270,23 +315,33 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT * If something unexpected happens, the [Callback.onFailure] method will be called with the error. Some devices are not compatible * at all with the cryptographic implementation and will have [CredentialsManagerException.isDeviceIncompatible] return true. * + * If the user's lock screen authentication configuration matches the authentication level specified in the [authenticationOptions], + * the user will be prompted to authenticate before accessing the credentials. * - * If a LockScreen is setup and [SecureCredentialsManager.requireAuthentication] was called, the user will be asked to authenticate before accessing - * the credentials. Your activity must override the [Activity.onActivityResult] method and call - * [SecureCredentialsManager.checkAuthenticationResult] with the received values. - * + * @param fragmentActivity the activity that will be used to host the [androidx.biometric.BiometricPrompt] + * @param authenticationOptions, an instance of [LocalAuthenticationOptions] used to configure authentication performed using [androidx.biometric.BiometricPrompt] * @param scope the scope to request for the access token. If null is passed, the previous scope will be kept. * @param minTtl the minimum time in seconds that the access token should last before expiration. * @param parameters additional parameters to send in the request to refresh expired credentials * @param callback the callback to receive the result in. */ public fun getCredentials( + fragmentActivity: FragmentActivity, + authenticationOptions: LocalAuthenticationOptions, scope: String?, minTtl: Int, parameters: Map, callback: Callback ) { - getCredentials(scope, minTtl, parameters, false, callback) + getCredentials( + fragmentActivity, + authenticationOptions, + scope, + minTtl, + parameters, + false, + callback + ) } /** @@ -295,24 +350,79 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT * at all with the cryptographic implementation and will have [CredentialsManagerException.isDeviceIncompatible] return true. * * - * If a LockScreen is setup and [SecureCredentialsManager.requireAuthentication] was called, the user will be asked to authenticate before accessing - * the credentials. Your activity must override the [Activity.onActivityResult] method and call - * [SecureCredentialsManager.checkAuthenticationResult] with the received values. + * If the user's lock screen authentication configuration matches the authentication level specified in the [authenticationOptions], + * the user will be prompted to authenticate before accessing the credentials. * + * @param fragmentActivity the activity that will be used to host the [androidx.biometric.BiometricPrompt] + * @param authenticationOptions, an instance of [LocalAuthenticationOptions] used to configure authentication performed using [androidx.biometric.BiometricPrompt] * @param scope the scope to request for the access token. If null is passed, the previous scope will be kept. * @param minTtl the minimum time in seconds that the access token should last before expiration. - * @param parameters additional parameters to send in the request to refresh expired credentials. + * @param parameters additional parameters to send in the request to refresh expired credentials * @param forceRefresh this will avoid returning the existing credentials and retrieves a new one even if valid credentials exist. * @param callback the callback to receive the result in. */ public fun getCredentials( + fragmentActivity: FragmentActivity, + authenticationOptions: LocalAuthenticationOptions, scope: String?, minTtl: Int, parameters: Map, forceRefresh: Boolean, callback: Callback ) { - getCredentials(scope, minTtl, parameters, mapOf(), forceRefresh, callback) + getCredentials( + fragmentActivity, + authenticationOptions, + scope, + minTtl, + parameters, + mapOf(), + forceRefresh, + callback + ) + } + + /** + * Tries to obtain the credentials from the Storage. The callback's [Callback.onSuccess] method will be called with the result. + * If something unexpected happens, the [Callback.onFailure] method will be called with the error. Some devices are not compatible + * at all with the cryptographic implementation and will have [CredentialsManagerException.isDeviceIncompatible] return true. + * + * + * If the user's lock screen authentication configuration matches the authentication level specified in the [authenticationOptions], + * the user will be prompted to authenticate before accessing the credentials. + * + * @param fragmentActivity the activity that will be used to host the [androidx.biometric.BiometricPrompt] + * @param authenticationOptions, an instance of [LocalAuthenticationOptions] used to configure authentication performed using [androidx.biometric.BiometricPrompt] + * @param scope the scope to request for the access token. If null is passed, the previous scope will be kept. + * @param minTtl the minimum time in seconds that the access token should last before expiration. + * @param parameters additional parameters to send in the request to refresh expired credentials. + * @param forceRefresh this will avoid returning the existing credentials and retrieves a new one even if valid credentials exist. + * @param callback the callback to receive the result in. + */ + public fun getCredentials( + fragmentActivity: FragmentActivity, + authenticationOptions: LocalAuthenticationOptions, + scope: String? = null, + minTtl: Int = 0, + parameters: Map = emptyMap(), + headers: Map = emptyMap(), + forceRefresh: Boolean = false, + callback: Callback + ) { + + val localAuthenticationManager = localAuthenticationManagerFactory.create( + activity = fragmentActivity, + authenticationOptions = authenticationOptions, + resultCallback = localAuthenticationResultCallback( + scope, + minTtl, + parameters, + headers, + forceRefresh, + callback + ) + ) + localAuthenticationManager.authenticate() } private val localAuthenticationResultCallback = @@ -331,35 +441,6 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT } } - public fun getCredentialsWithAuthentication( - activity: FragmentActivity, - authenticationOptions: LocalAuthenticationOptions, - scope: String?, - minTtl: Int, - parameters: Map, - headers: Map, - forceRefresh: Boolean, - callback: Callback - ) { - - val localAuthenticationManager = LocalAuthenticationManager( - activity, - authenticationOptions, - serialExecutor - ) - - localAuthenticationManager.authenticate( - localAuthenticationResultCallback( - scope, - minTtl, - parameters, - headers, - forceRefresh, - callback - ) - ) - } - /** * Delete the stored credentials */ @@ -402,7 +483,8 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT (canRefresh == null || !canRefresh)) } - private fun getCredentials( + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun getCredentials( scope: String?, minTtl: Int, parameters: Map, From 1363d8d2b34ce08b5f736c792677ea8769792d9d Mon Sep 17 00:00:00 2001 From: Sai Venkat Desu Date: Mon, 8 Jul 2024 18:48:15 +0530 Subject: [PATCH 12/30] test: added unit tests for LocalAuthenticationManager Signed-off-by: Sai Venkat Desu --- auth0/build.gradle | 4 +- .../storage/LocalAuthenticationManagerTest.kt | 606 ++++++++++++++++++ 2 files changed, 609 insertions(+), 1 deletion(-) create mode 100644 auth0/src/test/java/com/auth0/android/authentication/storage/LocalAuthenticationManagerTest.kt diff --git a/auth0/build.gradle b/auth0/build.gradle index 25083e5f..22897333 100644 --- a/auth0/build.gradle +++ b/auth0/build.gradle @@ -105,9 +105,11 @@ dependencies { testImplementation "com.squareup.okhttp3:mockwebserver:$okhttpVersion" testImplementation "com.squareup.okhttp3:okhttp-tls:$okhttpVersion" testImplementation 'com.jayway.awaitility:awaitility:1.7.0' - testImplementation 'org.robolectric:robolectric:4.6.1' + testImplementation 'org.robolectric:robolectric:4.8.1' testImplementation 'androidx.test.espresso:espresso-intents:3.5.1' testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion" + + testImplementation "androidx.biometric:biometric:$biometricLibraryVersion" } apply from: rootProject.file('gradle/jacoco.gradle') diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/LocalAuthenticationManagerTest.kt b/auth0/src/test/java/com/auth0/android/authentication/storage/LocalAuthenticationManagerTest.kt new file mode 100644 index 00000000..594c7b2a --- /dev/null +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/LocalAuthenticationManagerTest.kt @@ -0,0 +1,606 @@ +package com.auth0.android.authentication.storage + +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricPrompt +import androidx.fragment.app.FragmentActivity +import com.auth0.android.callback.Callback +import com.nhaarman.mockitokotlin2.KArgumentCaptor +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.argumentCaptor +import com.nhaarman.mockitokotlin2.verify +import org.hamcrest.MatcherAssert +import org.hamcrest.Matchers +import org.hamcrest.core.Is +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + + +@RunWith(RobolectricTestRunner::class) +public class LocalAuthenticationManagerTest { + + private lateinit var fragmentActivity: FragmentActivity + + @Mock + private lateinit var biometricManager: BiometricManager + + @Mock + private lateinit var callback: Callback + + @Mock + private lateinit var authenticationResult: BiometricPrompt.AuthenticationResult + + private val exceptionCaptor: KArgumentCaptor = argumentCaptor() + + @Before + public fun setUp() { + MockitoAnnotations.openMocks(this) + fragmentActivity = + Mockito.spy( + Robolectric.buildActivity(FragmentActivity::class.java).create().start().resume() + .get() + ) + + } + + @Test + @Config(sdk = [29]) + public fun testFailureOfDeviceCredentialsAuthenticationLevelOnAPILessThan30() { + val localAuthenticationManager = LocalAuthenticationManager( + fragmentActivity, + getAuthenticationOptions(authenticator = AuthenticationLevel.DEVICE_CREDENTIAL), + biometricManager, + callback + ) + + localAuthenticationManager.authenticate() + verify(callback).onFailure(exceptionCaptor.capture()) + + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat( + exception.message, + Is.`is`(CredentialsManagerException.BIOMETRIC_ERROR_DEVICE_CREDENTIAL_NOT_AVAILABLE.message) + ) + } + + @Test + @Config(sdk = [30]) + public fun testSuccessOfDeviceCredentialsAuthenticationLevelOnAPI30() { + val localAuthenticationManager = LocalAuthenticationManager( + fragmentActivity, + getAuthenticationOptions(authenticator = AuthenticationLevel.DEVICE_CREDENTIAL), + biometricManager, + callback + ) + + localAuthenticationManager.authenticate() + localAuthenticationManager.onAuthenticationSucceeded(authenticationResult) + val argumentCaptor = argumentCaptor() + verify(biometricManager).canAuthenticate(argumentCaptor.capture()) + verify(callback).onSuccess(true) + + val authenticationLevels = argumentCaptor.firstValue + MatcherAssert.assertThat(authenticationLevels, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat( + authenticationLevels, + Is.`is`(AuthenticationLevel.DEVICE_CREDENTIAL.value) + ) + } + + @Test + @Config(sdk = [28, 29]) + public fun testFailureOfBiometricStrongAndDeviceCredentialsFallbackOnAPILevel28and29() { + val localAuthenticationManager = LocalAuthenticationManager( + fragmentActivity, + getAuthenticationOptions( + authenticator = AuthenticationLevel.STRONG, + enableDeviceCredentialFallback = true + ), + biometricManager, + callback + ) + + localAuthenticationManager.authenticate() + verify(callback).onFailure(exceptionCaptor.capture()) + + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat( + exception, + Is.`is`(CredentialsManagerException.BIOMETRIC_ERROR_STRONG_AND_DEVICE_CREDENTIAL_NOT_AVAILABLE) + ) + } + + @Test + @Config(sdk = [27, 30]) + public fun testSuccessOfBiometricStrongAndDeviceCredentialsFallbackOnAPILevel27and30() { + val localAuthenticationManager = LocalAuthenticationManager( + fragmentActivity, + getAuthenticationOptions( + authenticator = AuthenticationLevel.STRONG, + enableDeviceCredentialFallback = true + ), + biometricManager, + callback + ) + + localAuthenticationManager.authenticate() + localAuthenticationManager.onAuthenticationSucceeded(authenticationResult) + val argumentCaptor = argumentCaptor() + verify(biometricManager).canAuthenticate(argumentCaptor.capture()) + verify(callback).onSuccess(true) + + val authenticationLevels = argumentCaptor.firstValue + MatcherAssert.assertThat(authenticationLevels, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat( + authenticationLevels, + Is.`is`(AuthenticationLevel.STRONG.value or AuthenticationLevel.DEVICE_CREDENTIAL.value) + ) + } + + @Test + @Config(sdk = [27, 28, 29, 30]) + public fun testSuccessOfBiometricWeakAndDeviceCredentialsFallbackAcrossMultipleLevels() { + val localAuthenticationManager = LocalAuthenticationManager( + fragmentActivity, + getAuthenticationOptions( + authenticator = AuthenticationLevel.WEAK, + enableDeviceCredentialFallback = true + ), + biometricManager, + callback + ) + + localAuthenticationManager.authenticate() + localAuthenticationManager.onAuthenticationSucceeded(authenticationResult) + val argumentCaptor = argumentCaptor() + verify(biometricManager).canAuthenticate(argumentCaptor.capture()) + verify(callback).onSuccess(true) + + val authenticationLevels = argumentCaptor.firstValue + MatcherAssert.assertThat(authenticationLevels, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat( + authenticationLevels, + Is.`is`(AuthenticationLevel.WEAK.value or AuthenticationLevel.DEVICE_CREDENTIAL.value) + ) + } + + @Test + @Config(sdk = [27, 28, 29, 30]) + public fun testSuccessOfBiometricStrongAuthenticationAcrossMultipleLevels() { + val localAuthenticationManager = LocalAuthenticationManager( + fragmentActivity, + getAuthenticationOptions(authenticator = AuthenticationLevel.STRONG), + biometricManager, + callback + ) + + localAuthenticationManager.authenticate() + localAuthenticationManager.onAuthenticationSucceeded(authenticationResult) + val argumentCaptor = argumentCaptor() + verify(biometricManager).canAuthenticate(argumentCaptor.capture()) + verify(callback).onSuccess(true) + + val authenticationLevels = argumentCaptor.firstValue + MatcherAssert.assertThat(authenticationLevels, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat( + authenticationLevels, + Is.`is`(AuthenticationLevel.STRONG.value) + ) + } + + @Test + @Config(sdk = [27, 28, 29, 30]) + public fun testSuccessOfBiometricWeakAuthenticationAcrossMultipleLevels() { + val localAuthenticationManager = LocalAuthenticationManager( + fragmentActivity, + getAuthenticationOptions(authenticator = AuthenticationLevel.WEAK), + biometricManager, + callback + ) + + localAuthenticationManager.authenticate() + localAuthenticationManager.onAuthenticationSucceeded(authenticationResult) + val argumentCaptor = argumentCaptor() + verify(biometricManager).canAuthenticate(argumentCaptor.capture()) + verify(callback).onSuccess(true) + + val authenticationLevels = argumentCaptor.firstValue + MatcherAssert.assertThat(authenticationLevels, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat( + authenticationLevels, + Is.`is`(AuthenticationLevel.WEAK.value) + ) + } + + + @Test + public fun testCanAuthenticateReturnsBIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED() { + val localAuthenticationManager = LocalAuthenticationManager( + fragmentActivity, + getAuthenticationOptions(), + biometricManager, + callback + ) + + Mockito.`when`(biometricManager.canAuthenticate(any())) + .thenReturn(BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED) + + localAuthenticationManager.authenticate() + verify(callback).onFailure(exceptionCaptor.capture()) + + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat( + exception.message, + Is.`is`(CredentialsManagerException.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED.message) + ) + } + + @Test + public fun testCanAuthenticateReturnsBIOMETRIC_ERROR_NO_HARDWARE() { + val localAuthenticationManager = LocalAuthenticationManager( + fragmentActivity, + getAuthenticationOptions(), + biometricManager, + callback + ) + + Mockito.`when`(biometricManager.canAuthenticate(any())) + .thenReturn(BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) + + localAuthenticationManager.authenticate() + verify(callback).onFailure(exceptionCaptor.capture()) + + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat( + exception.message, + Is.`is`(CredentialsManagerException.BIOMETRIC_ERROR_NO_HARDWARE.message) + ) + } + + @Test + public fun testCanAuthenticateReturnsBIOMETRIC_ERROR_NONE_ENROLLED() { + val localAuthenticationManager = LocalAuthenticationManager( + fragmentActivity, + getAuthenticationOptions(), + biometricManager, + callback + ) + + Mockito.`when`(biometricManager.canAuthenticate(any())) + .thenReturn(BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) + + localAuthenticationManager.authenticate() + verify(callback).onFailure(exceptionCaptor.capture()) + + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat( + exception.message, + Is.`is`(CredentialsManagerException.BIOMETRIC_ERROR_NONE_ENROLLED.message) + ) + } + + @Test + public fun testCanAuthenticateReturnsBIOMETRIC_ERROR_UNSUPPORTED() { + val localAuthenticationManager = LocalAuthenticationManager( + fragmentActivity, + getAuthenticationOptions(), + biometricManager, + callback + ) + + Mockito.`when`(biometricManager.canAuthenticate(any())) + .thenReturn(BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED) + + localAuthenticationManager.authenticate() + verify(callback).onFailure(exceptionCaptor.capture()) + + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat( + exception.message, + Is.`is`(CredentialsManagerException.BIOMETRIC_ERROR_UNSUPPORTED.message) + ) + } + + @Test + public fun testCanAuthenticateReturnsBIOMETRIC_ERROR_STATUS_UNKNOWN() { + val localAuthenticationManager = LocalAuthenticationManager( + fragmentActivity, + getAuthenticationOptions(), + biometricManager, + callback + ) + + Mockito.`when`(biometricManager.canAuthenticate(any())) + .thenReturn(BiometricManager.BIOMETRIC_STATUS_UNKNOWN) + + localAuthenticationManager.authenticate() + verify(callback).onFailure(exceptionCaptor.capture()) + + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat( + exception.message, + Is.`is`(CredentialsManagerException.BIOMETRIC_ERROR_STATUS_UNKNOWN.message) + ) + } + + @Test + public fun testCanAuthenticateReturnsBIOMETRIC_ERROR_HW_UNAVAILABLE() { + val localAuthenticationManager = LocalAuthenticationManager( + fragmentActivity, + getAuthenticationOptions(), + biometricManager, + callback + ) + + Mockito.`when`(biometricManager.canAuthenticate(any())) + .thenReturn(BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE) + + localAuthenticationManager.authenticate() + verify(callback).onFailure(exceptionCaptor.capture()) + + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat( + exception.message, + Is.`is`(CredentialsManagerException.BIOMETRIC_ERROR_HW_UNAVAILABLE.message) + ) + } + + @Test + public fun testInvalidAuthenticationScenario() { + val localAuthenticationManager = LocalAuthenticationManager( + fragmentActivity, + getAuthenticationOptions(), + biometricManager, + callback + ) + + localAuthenticationManager.authenticate() + Mockito.`when`(biometricManager.canAuthenticate(any())) + .thenReturn(BiometricManager.BIOMETRIC_SUCCESS) + localAuthenticationManager.onAuthenticationFailed() + verify(callback).onFailure(CredentialsManagerException.BIOMETRICS_INVALID_USER) + } + + @Test + public fun testAuthenticationErrorWithBIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL() { + val localAuthenticationManager = LocalAuthenticationManager( + fragmentActivity, + getAuthenticationOptions(), + biometricManager, + callback + ) + + localAuthenticationManager.authenticate() + localAuthenticationManager.onAuthenticationError( + BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL, + "error" + ) + verify(callback).onFailure(CredentialsManagerException.BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL) + } + + @Test + public fun testAuthenticationErrorWithBIOMETRIC_ERROR_NEGATIVE_BUTTON() { + val localAuthenticationManager = LocalAuthenticationManager( + fragmentActivity, + getAuthenticationOptions(), + biometricManager, + callback + ) + + localAuthenticationManager.authenticate() + localAuthenticationManager.onAuthenticationError( + BiometricPrompt.ERROR_NEGATIVE_BUTTON, + "error" + ) + verify(callback).onFailure(CredentialsManagerException.BIOMETRIC_ERROR_NEGATIVE_BUTTON) + } + + @Test + public fun testAuthenticationErrorWithBIOMETRIC_ERROR_HW_NOT_PRESENT() { + val localAuthenticationManager = LocalAuthenticationManager( + fragmentActivity, + getAuthenticationOptions(), + biometricManager, + callback + ) + + localAuthenticationManager.authenticate() + localAuthenticationManager.onAuthenticationError( + BiometricPrompt.ERROR_HW_NOT_PRESENT, + "error" + ) + verify(callback).onFailure(CredentialsManagerException.BIOMETRIC_ERROR_HW_NOT_PRESENT) + } + + @Test + public fun testAuthenticationErrorWithBIOMETRIC_ERROR_NO_BIOMETRICS() { + val localAuthenticationManager = LocalAuthenticationManager( + fragmentActivity, + getAuthenticationOptions(), + biometricManager, + callback + ) + + localAuthenticationManager.authenticate() + localAuthenticationManager.onAuthenticationError( + BiometricPrompt.ERROR_NO_BIOMETRICS, + "error" + ) + verify(callback).onFailure(CredentialsManagerException.BIOMETRIC_ERROR_NO_BIOMETRICS) + } + + @Test + public fun testAuthenticationErrorWithBIOMETRIC_ERROR_USER_CANCELED() { + val localAuthenticationManager = LocalAuthenticationManager( + fragmentActivity, + getAuthenticationOptions(), + biometricManager, + callback + ) + + localAuthenticationManager.authenticate() + localAuthenticationManager.onAuthenticationError( + BiometricPrompt.ERROR_USER_CANCELED, + "error" + ) + verify(callback).onFailure(CredentialsManagerException.BIOMETRIC_ERROR_USER_CANCELED) + } + + + @Test + public fun testAuthenticationErrorWithBIOMETRIC_ERROR_LOCKOUT_PERMANENT() { + val localAuthenticationManager = LocalAuthenticationManager( + fragmentActivity, + getAuthenticationOptions(), + biometricManager, + callback + ) + + localAuthenticationManager.authenticate() + localAuthenticationManager.onAuthenticationError( + BiometricPrompt.ERROR_LOCKOUT_PERMANENT, + "error" + ) + verify(callback).onFailure(CredentialsManagerException.BIOMETRIC_ERROR_LOCKOUT_PERMANENT) + } + + @Test + public fun testAuthenticationErrorWithBIOMETRIC_ERROR_VENDOR() { + val localAuthenticationManager = LocalAuthenticationManager( + fragmentActivity, + getAuthenticationOptions(), + biometricManager, + callback + ) + + localAuthenticationManager.authenticate() + localAuthenticationManager.onAuthenticationError(BiometricPrompt.ERROR_VENDOR, "error") + verify(callback).onFailure(CredentialsManagerException.BIOMETRIC_ERROR_VENDOR) + } + + @Test + public fun testAuthenticationErrorWithBIOMETRIC_ERROR_LOCKOUT() { + val localAuthenticationManager = LocalAuthenticationManager( + fragmentActivity, + getAuthenticationOptions(), + biometricManager, + callback + ) + + localAuthenticationManager.authenticate() + localAuthenticationManager.onAuthenticationError(BiometricPrompt.ERROR_LOCKOUT, "error") + verify(callback).onFailure(CredentialsManagerException.BIOMETRIC_ERROR_LOCKOUT) + } + + @Test + public fun testAuthenticationErrorWithBIOMETRIC_ERROR_CANCELED() { + val localAuthenticationManager = LocalAuthenticationManager( + fragmentActivity, + getAuthenticationOptions(), + biometricManager, + callback + ) + + localAuthenticationManager.authenticate() + localAuthenticationManager.onAuthenticationError(BiometricPrompt.ERROR_CANCELED, "error") + verify(callback).onFailure(CredentialsManagerException.BIOMETRIC_ERROR_CANCELED) + } + + @Test + public fun testAuthenticationErrorWithBIOMETRIC_ERROR_NO_SPACE() { + val localAuthenticationManager = LocalAuthenticationManager( + fragmentActivity, + getAuthenticationOptions(), + biometricManager, + callback + ) + + localAuthenticationManager.authenticate() + localAuthenticationManager.onAuthenticationError(BiometricPrompt.ERROR_NO_SPACE, "error") + verify(callback).onFailure(CredentialsManagerException.BIOMETRIC_ERROR_NO_SPACE) + } + + @Test + public fun testAuthenticationErrorWithBIOMETRIC_ERROR_TIMEOUT() { + val localAuthenticationManager = LocalAuthenticationManager( + fragmentActivity, + getAuthenticationOptions(), + biometricManager, + callback + ) + + localAuthenticationManager.authenticate() + localAuthenticationManager.onAuthenticationError(BiometricPrompt.ERROR_TIMEOUT, "error") + verify(callback).onFailure(CredentialsManagerException.BIOMETRIC_ERROR_TIMEOUT) + } + + @Test + public fun testAuthenticationErrorWithBIOMETRIC_ERROR_UNABLE_TO_PROCESS() { + val localAuthenticationManager = LocalAuthenticationManager( + fragmentActivity, + getAuthenticationOptions(), + biometricManager, + callback + ) + + localAuthenticationManager.authenticate() + localAuthenticationManager.onAuthenticationError( + BiometricPrompt.ERROR_UNABLE_TO_PROCESS, + "error" + ) + verify(callback).onFailure(CredentialsManagerException.BIOMETRIC_ERROR_UNABLE_TO_PROCESS) + } + + @Test + public fun testAuthenticationErrorWithBIOMETRIC_ERROR_HW_UNAVAILABLE() { + val localAuthenticationManager = LocalAuthenticationManager( + fragmentActivity, + getAuthenticationOptions(), + biometricManager, + callback + ) + + localAuthenticationManager.authenticate() + localAuthenticationManager.onAuthenticationError( + BiometricPrompt.ERROR_HW_UNAVAILABLE, + "error" + ) + verify(callback).onFailure(CredentialsManagerException.BIOMETRIC_ERROR_HW_UNAVAILABLE) + } + + private fun getAuthenticationOptions( + title: String = "title", + description: String = "description", + subtitle: String = "subtitle", + negativeButtonText: String = "negativeButtonText", + authenticator: AuthenticationLevel = AuthenticationLevel.STRONG, + enableDeviceCredentialFallback: Boolean = false + ): LocalAuthenticationOptions { + + val builder = LocalAuthenticationOptions.Builder() + builder.apply { + title(title) + subtitle(subtitle) + description(description) + negativeButtonText(negativeButtonText) + authenticator(authenticator) + enableDeviceCredentialFallback(enableDeviceCredentialFallback) + } + return builder.build() + } +} + From 4bb0bd7d14392c3af500d19f50728ff68eb1f1ac Mon Sep 17 00:00:00 2001 From: Sai Venkat Desu Date: Mon, 8 Jul 2024 18:48:55 +0530 Subject: [PATCH 13/30] test: updated unit tests in CredentialsManagerTest and SecureCredentialsManagerTest w.r.t to Biometric Prompt changes Signed-off-by: Sai Venkat Desu --- .../storage/CredentialsManagerTest.kt | 2 +- .../storage/SecureCredentialsManagerTest.kt | 963 +++++++++--------- 2 files changed, 481 insertions(+), 484 deletions(-) diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt index 19cd0cbf..eda4d1ad 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt @@ -174,7 +174,7 @@ public class CredentialsManagerTest { @Test public fun shouldThrowOnSetIfCredentialsDoesNotHaveIdTokenOrAccessToken() { exception.expect(CredentialsManagerException::class.java) - exception.expectMessage("Credentials must have a valid date of expiration and a valid access_token or id_token value.") + exception.expectMessage("Credentials must have a valid access_token or id_token value.") val credentials: Credentials = CredentialsMock("", "", "type", "refreshToken", Date(), null) manager.saveCredentials(credentials) diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt index 17d6075a..c0a8bc96 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt @@ -3,16 +3,8 @@ package com.auth0.android.authentication.storage import android.app.Activity import android.app.KeyguardManager import android.content.Context -import android.content.Intent -import android.os.Build.VERSION import android.util.Base64 -import androidx.activity.ComponentActivity -import androidx.activity.result.ActivityResult -import androidx.activity.result.ActivityResultCallback -import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.ActivityResultRegistry -import androidx.activity.result.contract.ActivityResultContract -import androidx.core.app.ActivityOptionsCompat +import androidx.fragment.app.FragmentActivity import com.auth0.android.Auth0 import com.auth0.android.authentication.AuthenticationAPIClient import com.auth0.android.authentication.AuthenticationException @@ -40,15 +32,12 @@ import org.junit.Rule import org.junit.Test import org.junit.rules.ExpectedException import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.* import org.mockito.Mock import org.mockito.Mockito import org.mockito.MockitoAnnotations import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.robolectric.util.ReflectionHelpers import java.lang.reflect.Modifier import java.util.* import java.util.concurrent.Executor @@ -76,6 +65,14 @@ public class SecureCredentialsManagerTest { @Mock private lateinit var jwtDecoder: JWTDecoder + @Mock + private lateinit var factory: LocalAuthenticationManagerFactory + + @Mock + private lateinit var localAuthenticationManager: LocalAuthenticationManager + + private lateinit var fragmentActivity: FragmentActivity + private val serialExecutor = Executor { runnable -> runnable.run() } private val credentialsCaptor: KArgumentCaptor = argumentCaptor() @@ -98,8 +95,19 @@ public class SecureCredentialsManagerTest { val kManager = mock() Mockito.`when`(activityContext.getSystemService(Context.KEYGUARD_SERVICE)) .thenReturn(kManager) + Mockito.`when`(factory.create(any(), any(), any())).thenAnswer { invocation -> + val callback = invocation.arguments[2] as Callback + Mockito.`when`(localAuthenticationManager.resultCallback) + .thenReturn(callback) + return@thenAnswer localAuthenticationManager + } + fragmentActivity = + Mockito.spy( + Robolectric.buildActivity(FragmentActivity::class.java).create().start().resume() + .get() + ) val secureCredentialsManager = - SecureCredentialsManager(client, storage, crypto, jwtDecoder, serialExecutor) + SecureCredentialsManager(client, storage, crypto, jwtDecoder, serialExecutor, factory) manager = Mockito.spy(secureCredentialsManager) Mockito.doReturn(CredentialsMock.CURRENT_TIME_MS).`when`(manager).currentTimeInMillis gson = GsonProvider.gson @@ -284,7 +292,7 @@ public class SecureCredentialsManagerTest { MatcherAssert.assertThat(exception!!.isDeviceIncompatible, Is.`is`(false)) MatcherAssert.assertThat( exception.message, - Is.`is`("A change on the Lock Screen security settings have deemed the encryption keys invalid and have been recreated. Please try saving the credentials again.") + Is.`is`("A change on the Lock Screen security settings have deemed the encryption keys invalid and have been recreated. Any previously stored content is now lost. Please try saving the credentials again.") ) verify(storage).remove("com.auth0.credentials") verify(storage).remove("com.auth0.credentials_expires_at") @@ -322,7 +330,7 @@ public class SecureCredentialsManagerTest { @Test public fun shouldThrowOnSaveIfCredentialsDoesNotHaveIdTokenOrAccessToken() { exception.expect(CredentialsManagerException::class.java) - exception.expectMessage("Credentials must have a valid date of expiration and a valid access_token or id_token value.") + exception.expectMessage("Credentials must have a valid access_token or id_token value.") val credentials: Credentials = CredentialsMock("", "", "type", "refreshToken", Date(), "scope") manager.saveCredentials(credentials) @@ -363,7 +371,7 @@ public class SecureCredentialsManagerTest { ) Mockito.`when`(crypto.decrypt(storedJson.toByteArray())) .thenThrow(CryptoException("err", null)) - manager.getCredentials(callback) + manager.getCredentials(null, 0, emptyMap(), emptyMap(), false, callback) verify(callback).onFailure( exceptionCaptor.capture() ) @@ -398,7 +406,7 @@ public class SecureCredentialsManagerTest { ) Mockito.`when`(crypto.decrypt(storedJson.toByteArray())) .thenThrow(IncompatibleDeviceException(null)) - manager.getCredentials(callback) + manager.getCredentials(null, 0, emptyMap(), emptyMap(), false, callback) verify(callback).onFailure( exceptionCaptor.capture() ) @@ -438,7 +446,7 @@ public class SecureCredentialsManagerTest { val expectedJson = gson.toJson(expectedCredentials) Mockito.`when`(crypto.encrypt(expectedJson.toByteArray())) .thenThrow(CryptoException("CryptoException is thrown")) - manager.getCredentials("different scope", 0, callback) // minTTL of 0 seconds (default) + manager.getCredentials("different scope", 0, emptyMap(), emptyMap(), false, callback) // minTTL of 0 seconds (default) verify(request) .addParameter(eq("scope"), eq("different scope")) verify(callback).onFailure( @@ -449,8 +457,14 @@ public class SecureCredentialsManagerTest { val exception = exceptionCaptor.firstValue val retrievedCredentials = exception.refreshedCredentials MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) - MatcherAssert.assertThat(exception.message, Is.`is`("An error occurred while saving the refreshed Credentials.")) - MatcherAssert.assertThat(exception.cause!!.message, Is.`is`("A change on the Lock Screen security settings have deemed the encryption keys invalid and have been recreated. Please try saving the credentials again.")) + MatcherAssert.assertThat( + exception.message, + Is.`is`("An error occurred while saving the refreshed Credentials.") + ) + MatcherAssert.assertThat( + exception.cause!!.message, + Is.`is`("A change on the Lock Screen security settings have deemed the encryption keys invalid and have been recreated. Any previously stored content is now lost. Please try saving the credentials again.") + ) MatcherAssert.assertThat(retrievedCredentials, Is.`is`(Matchers.notNullValue())) MatcherAssert.assertThat(retrievedCredentials!!.idToken, Is.`is`("newId")) MatcherAssert.assertThat(retrievedCredentials.accessToken, Is.`is`("newAccess")) @@ -480,7 +494,7 @@ public class SecureCredentialsManagerTest { val expectedJson = gson.toJson(expectedCredentials) Mockito.`when`(crypto.encrypt(expectedJson.toByteArray())) .thenThrow(IncompatibleDeviceException(Exception())) - manager.getCredentials("different scope", 0, callback) // minTTL of 0 seconds (default) + manager.getCredentials("different scope", 0, emptyMap(), emptyMap(), false, callback) // minTTL of 0 seconds (default) verify(request) .addParameter(eq("scope"), eq("different scope")) verify(callback).onFailure( @@ -491,8 +505,14 @@ public class SecureCredentialsManagerTest { val exception = exceptionCaptor.firstValue val retrievedCredentials = exception.refreshedCredentials MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) - MatcherAssert.assertThat(exception.message, Is.`is`("An error occurred while saving the refreshed Credentials.")) - MatcherAssert.assertThat(exception.cause!!.message, Is.`is`("This device is not compatible with the SecureCredentialsManager class.")) + MatcherAssert.assertThat( + exception.message, + Is.`is`("An error occurred while saving the refreshed Credentials.") + ) + MatcherAssert.assertThat( + exception.cause!!.message, + Is.`is`("This device is not compatible with the SecureCredentialsManager class.") + ) MatcherAssert.assertThat(retrievedCredentials, Is.`is`(Matchers.notNullValue())) MatcherAssert.assertThat(retrievedCredentials!!.idToken, Is.`is`("newId")) MatcherAssert.assertThat(retrievedCredentials.accessToken, Is.`is`("newAccess")) @@ -520,7 +540,7 @@ public class SecureCredentialsManagerTest { Credentials("", "", "newType", "refreshToken", newDate, "different scope") Mockito.`when`(request.execute()).thenReturn(expectedCredentials) - manager.getCredentials("different scope", 0, callback) // minTTL of 0 seconds (default) + manager.getCredentials("different scope", 0, emptyMap(), emptyMap(), false, callback) // minTTL of 0 seconds (default) verify(request) .addParameter(eq("scope"), eq("different scope")) verify(callback).onFailure( @@ -531,8 +551,14 @@ public class SecureCredentialsManagerTest { val exception = exceptionCaptor.firstValue val retrievedCredentials = exception.refreshedCredentials MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) - MatcherAssert.assertThat(exception.message, Is.`is`("An error occurred while saving the refreshed Credentials.")) - MatcherAssert.assertThat(exception.cause!!.message, Is.`is`("Credentials must have a valid date of expiration and a valid access_token or id_token value.")) + MatcherAssert.assertThat( + exception.message, + Is.`is`("An error occurred while saving the refreshed Credentials.") + ) + MatcherAssert.assertThat( + exception.cause!!.message, + Is.`is`("Credentials must have a valid access_token or id_token value.") + ) MatcherAssert.assertThat(retrievedCredentials, Is.`is`(Matchers.nullValue())) } @@ -541,7 +567,7 @@ public class SecureCredentialsManagerTest { verifyNoMoreInteractions(client) val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) insertTestCredentials(false, false, true, expiresAt, "scope") - manager.getCredentials(callback) + manager.getCredentials(null, 0, emptyMap(), emptyMap(), false, callback) verify(callback).onFailure( exceptionCaptor.capture() ) @@ -555,13 +581,16 @@ public class SecureCredentialsManagerTest { verifyNoMoreInteractions(client) val expiresAt = Date(CredentialsMock.CURRENT_TIME_MS) //Same as current time --> expired insertTestCredentials(true, true, false, expiresAt, "scope") - manager.getCredentials(callback) + manager.getCredentials(null, 0, emptyMap(), emptyMap(), false, callback) verify(callback).onFailure( exceptionCaptor.capture() ) val exception = exceptionCaptor.firstValue MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) - MatcherAssert.assertThat(exception.message, Is.`is`("No Credentials were previously set.")) + MatcherAssert.assertThat( + exception.message, + Is.`is`("Credentials need to be renewed but no Refresh Token is available to renew them.") + ) } @Test @@ -569,7 +598,7 @@ public class SecureCredentialsManagerTest { verifyNoMoreInteractions(client) val expiresAt = Date(CredentialsMock.CURRENT_TIME_MS + ONE_HOUR_SECONDS * 1000) insertTestCredentials(true, true, true, expiresAt, "scope") - manager.getCredentials(callback) + manager.getCredentials(null, 0, emptyMap(), emptyMap(), false, callback) verify(callback).onSuccess( credentialsCaptor.capture() ) @@ -587,31 +616,40 @@ public class SecureCredentialsManagerTest { @Test @ExperimentalCoroutinesApi public fun shouldAwaitNonExpiredCredentialsFromStorage(): Unit = runTest { - verifyNoMoreInteractions(client) - val expiresAt = Date(CredentialsMock.CURRENT_TIME_MS + ONE_HOUR_SECONDS * 1000) - insertTestCredentials(true, true, true, expiresAt, "scope") - val retrievedCredentials = manager.awaitCredentials() - MatcherAssert.assertThat(retrievedCredentials, Is.`is`(Matchers.notNullValue())) - MatcherAssert.assertThat(retrievedCredentials.accessToken, Is.`is`("accessToken")) - MatcherAssert.assertThat(retrievedCredentials.idToken, Is.`is`("idToken")) - MatcherAssert.assertThat(retrievedCredentials.refreshToken, Is.`is`("refreshToken")) - MatcherAssert.assertThat(retrievedCredentials.type, Is.`is`("type")) - MatcherAssert.assertThat(retrievedCredentials.expiresAt, Is.`is`(Matchers.notNullValue())) - MatcherAssert.assertThat(retrievedCredentials.expiresAt.time, Is.`is`(expiresAt.time)) - MatcherAssert.assertThat(retrievedCredentials.scope, Is.`is`("scope")) + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } + verifyNoMoreInteractions(client) + val expiresAt = Date(CredentialsMock.CURRENT_TIME_MS + ONE_HOUR_SECONDS * 1000) + insertTestCredentials(true, true, true, expiresAt, "scope") + val retrievedCredentials = manager.awaitCredentials(fragmentActivity, getAuthenticationOptions()) + MatcherAssert.assertThat(retrievedCredentials, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat(retrievedCredentials.accessToken, Is.`is`("accessToken")) + MatcherAssert.assertThat(retrievedCredentials.idToken, Is.`is`("idToken")) + MatcherAssert.assertThat(retrievedCredentials.refreshToken, Is.`is`("refreshToken")) + MatcherAssert.assertThat(retrievedCredentials.type, Is.`is`("type")) + MatcherAssert.assertThat(retrievedCredentials.expiresAt, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat(retrievedCredentials.expiresAt.time, Is.`is`(expiresAt.time)) + MatcherAssert.assertThat(retrievedCredentials.scope, Is.`is`("scope")) } @Test @ExperimentalCoroutinesApi public fun shouldFailOnAwaitCredentialsWhenExpiredAndNoRefreshTokenWasSaved(): Unit = runTest { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } verifyNoMoreInteractions(client) val expiresAt = Date(CredentialsMock.CURRENT_TIME_MS) //Same as current time --> expired insertTestCredentials(true, true, false, expiresAt, "scope") val exception = assertThrows(CredentialsManagerException::class.java) { - runBlocking { manager.awaitCredentials() } + runBlocking { manager.awaitCredentials(fragmentActivity, getAuthenticationOptions()) } } MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) - MatcherAssert.assertThat(exception.message, Is.`is`("No Credentials were previously set.")) + MatcherAssert.assertThat( + exception.message, + Is.`is`("Credentials need to be renewed but no Refresh Token is available to renew them.") + ) } @Test @@ -619,7 +657,7 @@ public class SecureCredentialsManagerTest { verifyNoMoreInteractions(client) val expiresAt = Date(CredentialsMock.CURRENT_TIME_MS + ONE_HOUR_SECONDS * 1000) insertTestCredentials(true, false, true, expiresAt, "scope") - manager.getCredentials(callback) + manager.getCredentials(null, 0, emptyMap(), emptyMap(), false, callback) verify(callback).onSuccess( credentialsCaptor.capture() ) @@ -639,7 +677,13 @@ public class SecureCredentialsManagerTest { verifyNoMoreInteractions(client) val expiresAt = Date(CredentialsMock.CURRENT_TIME_MS + ONE_HOUR_SECONDS * 1000) insertTestCredentials(false, true, true, expiresAt, "scope") - manager.getCredentials(callback) + manager.getCredentials( + null, + 0, + emptyMap(), + emptyMap(), + false, + callback) verify(callback).onSuccess( credentialsCaptor.capture() ) @@ -672,7 +716,7 @@ public class SecureCredentialsManagerTest { val expectedJson = gson.toJson(expectedCredentials) Mockito.`when`(crypto.encrypt(expectedJson.toByteArray())) .thenReturn(expectedJson.toByteArray()) - manager.getCredentials(null, 60, callback) // minTTL of 1 minute + manager.getCredentials(null, 60, emptyMap(), emptyMap(), false, callback) // minTTL of 1 minute verify(request, never()) .addParameter(eq("scope"), anyString()) verify(callback).onSuccess( @@ -734,7 +778,7 @@ public class SecureCredentialsManagerTest { val expectedJson = gson.toJson(expectedCredentials) Mockito.`when`(crypto.encrypt(expectedJson.toByteArray())) .thenReturn(expectedJson.toByteArray()) - manager.getCredentials(null, 60, callback) // minTTL of 1 minute + manager.getCredentials(null, 60, emptyMap(), emptyMap(), false, callback) // minTTL of 1 minute verify(request, never()) .addParameter(eq("scope"), anyString()) verify(callback).onFailure( @@ -779,7 +823,7 @@ public class SecureCredentialsManagerTest { val expectedJson = gson.toJson(expectedCredentials) Mockito.`when`(crypto.encrypt(expectedJson.toByteArray())) .thenReturn(expectedJson.toByteArray()) - manager.getCredentials("different scope", 0, callback) // minTTL of 0 seconds (default) + manager.getCredentials("different scope", 0, emptyMap(), emptyMap(), false, callback) // minTTL of 0 seconds (default) verify(request) .addParameter(eq("scope"), eq("different scope")) verify(callback).onSuccess( @@ -840,7 +884,7 @@ public class SecureCredentialsManagerTest { val expectedJson = gson.toJson(expectedCredentials) Mockito.`when`(crypto.encrypt(expectedJson.toByteArray())) .thenReturn(expectedJson.toByteArray()) - manager.getCredentials("different scope", 0, callback) // minTTL of 0 seconds (default) + manager.getCredentials("different scope", 0, emptyMap(), emptyMap(), false, callback) // minTTL of 0 seconds (default) verify(request) .addParameter(eq("scope"), eq("different scope")) verify(callback).onSuccess( @@ -903,7 +947,7 @@ public class SecureCredentialsManagerTest { Mockito.`when`(crypto.encrypt(expectedJson.toByteArray())) .thenReturn(expectedJson.toByteArray()) - manager.getCredentials("different scope", 0, callback) // minTTL of 0 seconds (default) + manager.getCredentials("different scope", 0, emptyMap(), emptyMap(), false, callback) // minTTL of 0 seconds (default) verify(request) .addParameter(eq("scope"), eq("different scope")) verify(callback).onSuccess( @@ -984,7 +1028,13 @@ public class SecureCredentialsManagerTest { val expectedJson = gson.toJson(expectedCredentials) Mockito.`when`(crypto.encrypt(expectedJson.toByteArray())) .thenReturn(expectedJson.toByteArray()) - manager.getCredentials(callback) + manager.getCredentials( + null, + 0, + emptyMap(), + emptyMap(), + false, + callback) // requestCallbackCaptor.firstValue.onSuccess(renewedCredentials)TODO poovam verify(callback).onSuccess( credentialsCaptor.capture() @@ -1044,7 +1094,13 @@ public class SecureCredentialsManagerTest { val expectedJson = gson.toJson(renewedCredentials) Mockito.`when`(crypto.encrypt(expectedJson.toByteArray())) .thenReturn(expectedJson.toByteArray()) - manager.getCredentials(callback) + manager.getCredentials( + null, + 0, + emptyMap(), + emptyMap(), + false, + callback) verify(request, never()) .addParameter(eq("scope"), anyString()) verify(callback).onSuccess( @@ -1098,7 +1154,13 @@ public class SecureCredentialsManagerTest { //Trigger failure val authenticationException = mock() Mockito.`when`(request.execute()).thenThrow(authenticationException) - manager.getCredentials(callback) + manager.getCredentials( + null, + 0, + emptyMap(), + emptyMap(), + false, + callback) verify(callback).onFailure( exceptionCaptor.capture() ) @@ -1233,157 +1295,23 @@ public class SecureCredentialsManagerTest { /* * Authentication tests */ - @Test - public fun shouldThrowOnInvalidAuthenticationRequestCode() { - exception.expect(IllegalArgumentException::class.java) - exception.expectMessage("Request code must be a value between 1 and 255.") - val activity = - Robolectric.buildActivity(Activity::class.java).create().start().resume().get() - manager.requireAuthentication(activity, 256, null, null) - } @Test - @Config(sdk = [21]) - public fun shouldNotRequireAuthenticationIfAPI21AndLockScreenDisabled() { - ReflectionHelpers.setStaticField(VERSION::class.java, "SDK_INT", 21) - val activity = Mockito.spy( - Robolectric.buildActivity( - Activity::class.java - ).create().start().resume().get() - ) - - //Set LockScreen as Disabled - val kService = mock() - Mockito.`when`(activity.getSystemService(Context.KEYGUARD_SERVICE)).thenReturn(kService) - Mockito.`when`(kService.isKeyguardSecure).thenReturn(false) - Mockito.`when`(kService.createConfirmDeviceCredentialIntent("title", "description")) - .thenReturn(null) - val willAskAuthentication = - manager.requireAuthentication(activity, 123, "title", "description") - MatcherAssert.assertThat(willAskAuthentication, Is.`is`(false)) - } - - @Test - @Config(sdk = [23]) - public fun shouldNotRequireAuthenticationIfAPI23AndLockScreenDisabled() { - ReflectionHelpers.setStaticField(VERSION::class.java, "SDK_INT", 23) - val activity = Mockito.spy( - Robolectric.buildActivity( - Activity::class.java - ).create().start().resume().get() - ) - - //Set LockScreen as Disabled - val kService = mock() - Mockito.`when`(activity.getSystemService(Context.KEYGUARD_SERVICE)).thenReturn(kService) - Mockito.`when`(kService.isDeviceSecure).thenReturn(false) - Mockito.`when`(kService.createConfirmDeviceCredentialIntent("title", "description")) - .thenReturn(null) - val willAskAuthentication = - manager.requireAuthentication(activity, 123, "title", "description") - MatcherAssert.assertThat(willAskAuthentication, Is.`is`(false)) - } - - @Test - @Config(sdk = [21]) - public fun shouldRequireAuthenticationIfAPI21AndLockScreenEnabled() { - ReflectionHelpers.setStaticField(VERSION::class.java, "SDK_INT", 21) - val activity = Mockito.spy( - Robolectric.buildActivity( - Activity::class.java - ).create().start().resume().get() - ) - - //Set LockScreen as Enabled - val kService = mock() - Mockito.`when`(activity.getSystemService(Context.KEYGUARD_SERVICE)).thenReturn(kService) - Mockito.`when`(kService.isKeyguardSecure).thenReturn(true) - Mockito.`when`(kService.createConfirmDeviceCredentialIntent("title", "description")) - .thenReturn(Intent()) - val willAskAuthentication = - manager.requireAuthentication(activity, 123, "title", "description") - MatcherAssert.assertThat(willAskAuthentication, Is.`is`(true)) - } - - @Test - @Config(sdk = [23]) - public fun shouldRequireAuthenticationIfAPI23AndLockScreenEnabled() { - ReflectionHelpers.setStaticField(VERSION::class.java, "SDK_INT", 23) - val activity = Mockito.spy( - Robolectric.buildActivity( - Activity::class.java - ).create().start().resume().get() - ) - - //Set LockScreen as Enabled - val kService = mock() - Mockito.`when`(activity.getSystemService(Context.KEYGUARD_SERVICE)).thenReturn(kService) - Mockito.`when`(kService.isDeviceSecure).thenReturn(true) - Mockito.`when`(kService.createConfirmDeviceCredentialIntent("title", "description")) - .thenReturn(Intent()) - val willAskAuthentication = - manager.requireAuthentication(activity, 123, "title", "description") - MatcherAssert.assertThat(willAskAuthentication, Is.`is`(true)) - } - - @Test - public fun shouldGetCredentialsAfterAuthenticationUsingActivityResultsAPI() { + public fun shouldGetCredentialsWithAuthentication() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) insertTestCredentials(true, true, false, expiresAt, "scope") Mockito.`when`(storage.retrieveLong("com.auth0.credentials_expires_at")) .thenReturn(expiresAt.time) - val kService = mock() - val confirmCredentialsIntent = mock() - val contractCaptor = argumentCaptor>() - val callbackCaptor = argumentCaptor>() - - // Activity is "created" - val activityController = Robolectric.buildActivity( - ComponentActivity::class.java - ).create() - val activity = Mockito.spy(activityController.get()) - val successfulResult = ActivityResult(Activity.RESULT_OK, null) - val rRegistry = object : ActivityResultRegistry() { - override fun onLaunch( - requestCode: Int, - contract: ActivityResultContract, - input: I, - options: ActivityOptionsCompat? - ) { - MatcherAssert.assertThat(input, Is.`is`(confirmCredentialsIntent)) - dispatchResult(requestCode, successfulResult) - } - } - - Mockito.`when`(activity.getSystemService(Context.KEYGUARD_SERVICE)).thenReturn(kService) - Mockito.`when`(kService.isKeyguardSecure).thenReturn(true) - Mockito.`when`(activity.activityResultRegistry).thenReturn(rRegistry) - Mockito.`when`(kService.createConfirmDeviceCredentialIntent("theTitle", "theDescription")) - .thenReturn(confirmCredentialsIntent) - - //Require authentication - val willRequireAuthentication = - manager.requireAuthentication(activity, 123, "theTitle", "theDescription") - MatcherAssert.assertThat(willRequireAuthentication, Is.`is`(true)) - - Mockito.verify(activity) - .registerForActivityResult( - contractCaptor.capture(), - eq(rRegistry), - callbackCaptor.capture() - ) - - // Activity is "started" so pending ActivityResults are dispatched - activityController.start() - // Trigger the prompt for credentials - manager.getCredentials(callback) - verify(activity, never()).startActivityForResult(any(), anyInt()) - - //Continue after successful authentication - verify(callback).onSuccess( - credentialsCaptor.capture() + manager.getCredentials( + fragmentActivity, + getAuthenticationOptions(), + callback ) + verify(callback).onSuccess(credentialsCaptor.capture()) val retrievedCredentials = credentialsCaptor.firstValue MatcherAssert.assertThat(retrievedCredentials, Is.`is`(Matchers.notNullValue())) MatcherAssert.assertThat(retrievedCredentials.accessToken, Is.`is`("accessToken")) @@ -1393,335 +1321,366 @@ public class SecureCredentialsManagerTest { MatcherAssert.assertThat(retrievedCredentials.expiresAt, Is.`is`(Matchers.notNullValue())) MatcherAssert.assertThat(retrievedCredentials.expiresAt.time, Is.`is`(expiresAt.time)) MatcherAssert.assertThat(retrievedCredentials.scope, Is.`is`("scope")) + } - //A second call to (originally called internally) checkAuthenticationResult should fail as callback is set to null - val retryCheck = manager.checkAuthenticationResult(123, Activity.RESULT_OK) - MatcherAssert.assertThat(retryCheck, Is.`is`(false)) + @Test + public fun shouldNotGetCredentialsWhenCredentialsHaveExpired() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } + val credentialsExpiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) + val storedExpiresAt = Date(CredentialsMock.CURRENT_TIME_MS - ONE_HOUR_SECONDS * 1000) + insertTestCredentials(true, true, false, credentialsExpiresAt, "scope") + Mockito.`when`(storage.retrieveLong("com.auth0.credentials_access_token_expires_at")) + .thenReturn(storedExpiresAt.time) + manager.getCredentials( + fragmentActivity, + getAuthenticationOptions(), + callback + ) + // Should fail because of expired credentials + verify(callback).onFailure( + exceptionCaptor.capture() + ) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat(exception.message, Is.`is`("No Credentials were previously set.")) } @Test - public fun shouldNotGetCredentialsAfterCanceledAuthenticationUsingActivityResultsAPI() { + public fun shouldNotGetCredentialsWhenCredentialsWereCleared() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) insertTestCredentials(true, true, false, expiresAt, "scope") Mockito.`when`(storage.retrieveLong("com.auth0.credentials_expires_at")) .thenReturn(expiresAt.time) + Mockito.`when`(storage.retrieveString("com.auth0.credentials")).thenReturn(null) + manager.getCredentials( + fragmentActivity, + getAuthenticationOptions(), + callback + ) + // Return null for Credentials JSON since it is cleared + verify(callback).onFailure( + exceptionCaptor.capture() + ) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat(exception.message, Is.`is`("No Credentials were previously set.")) + } - val kService = mock() - val confirmCredentialsIntent = mock() - val contractCaptor = argumentCaptor>() - val callbackCaptor = argumentCaptor>() - - // Activity is "created" - val activityController = Robolectric.buildActivity( - ComponentActivity::class.java - ).create() - val activity = Mockito.spy(activityController.get()) - val canceledResult = ActivityResult(Activity.RESULT_CANCELED, null) - val rRegistry = object : ActivityResultRegistry() { - override fun onLaunch( - requestCode: Int, - contract: ActivityResultContract, - input: I, - options: ActivityOptionsCompat? - ) { - MatcherAssert.assertThat(input, Is.`is`(confirmCredentialsIntent)) - dispatchResult(requestCode, canceledResult) - } + @Test + public fun shouldNotGetCredentialsWhenBiometricHardwareUnavailable() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onFailure( + CredentialsManagerException.BIOMETRIC_ERROR_HW_UNAVAILABLE + ) } + manager.getCredentials( + fragmentActivity, + getAuthenticationOptions(), + callback + ) + // Should fail because of unavailable hardware + verify(callback).onFailure( + exceptionCaptor.capture() + ) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat( + exception.message, + Is.`is`("Cannot authenticate because the hardware is unavailable. Try again later.") + ) + } - Mockito.`when`(activity.getSystemService(Context.KEYGUARD_SERVICE)).thenReturn(kService) - Mockito.`when`(kService.isKeyguardSecure).thenReturn(true) - Mockito.`when`(activity.activityResultRegistry).thenReturn(rRegistry) - Mockito.`when`(kService.createConfirmDeviceCredentialIntent("theTitle", "theDescription")) - .thenReturn(confirmCredentialsIntent) - - // Require authentication - val willRequireAuthentication = - manager.requireAuthentication(activity, 123, "theTitle", "theDescription") - MatcherAssert.assertThat(willRequireAuthentication, Is.`is`(true)) - - Mockito.verify(activity) - .registerForActivityResult( - contractCaptor.capture(), - eq(rRegistry), - callbackCaptor.capture() + @Test + public fun shouldNotGetCredentialsWhenBiometricsUnableToProcess() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onFailure( + CredentialsManagerException.BIOMETRIC_ERROR_UNABLE_TO_PROCESS ) - - // Activity is "started" so pending ActivityResults are dispatched - activityController.start() - // Trigger the prompt for credentials - manager.getCredentials(callback) - verify(activity, never()).startActivityForResult(any(), anyInt()) - verify(callback, never()).onSuccess(any()) + } + manager.getCredentials( + fragmentActivity, + getAuthenticationOptions(), + callback + ) + // Should fail because of unable to process verify(callback).onFailure( exceptionCaptor.capture() ) - MatcherAssert.assertThat(exceptionCaptor.firstValue, Is.`is`(Matchers.notNullValue())) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) MatcherAssert.assertThat( - exceptionCaptor.firstValue.message, - Is.`is`("The user didn't pass the authentication challenge.") + exception.message, + Is.`is`("Failed to authenticate because the sensor was unable to process the current image.") ) } @Test - public fun shouldNotThrowWhenRequiringAuthenticationAfterStartingTheActivity() { - val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) - insertTestCredentials(true, true, false, expiresAt, "scope") - Mockito.`when`(storage.retrieveLong("com.auth0.credentials_expires_at")) - .thenReturn(expiresAt.time) - - /* - * Activity is "started" - This is a ComponentActivity, but the "registerForActivityResult" - * will not be called due to the lifecycle state - */ - val activity = Mockito.spy( - Robolectric.buildActivity( - ComponentActivity::class.java - ).create().resume().start().get() - ) - val kService = mock() - Mockito.`when`(activity.getSystemService(Context.KEYGUARD_SERVICE)).thenReturn(kService) - Mockito.`when`(kService.isKeyguardSecure).thenReturn(true) - val confirmCredentialsIntent = mock() - Mockito.`when`(kService.createConfirmDeviceCredentialIntent("theTitle", "theDescription")) - .thenReturn(confirmCredentialsIntent) - // Require authentication - val willRequireAuthentication = - manager.requireAuthentication(activity, 123, "theTitle", "theDescription") - MatcherAssert.assertThat(willRequireAuthentication, Is.`is`(true)) - manager.getCredentials(callback) - val intentCaptor = ArgumentCaptor.forClass(Intent::class.java) - verify(activity) - .startActivityForResult(intentCaptor.capture(), eq(123)) - MatcherAssert.assertThat(intentCaptor.value, Is.`is`(confirmCredentialsIntent)) - - // Continue after successful authentication - val processed = manager.checkAuthenticationResult(123, Activity.RESULT_OK) - MatcherAssert.assertThat(processed, Is.`is`(true)) - verify(callback).onSuccess( - credentialsCaptor.capture() + public fun shouldNotGetCredentialsWhenBiometricsTimeout() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onFailure( + CredentialsManagerException.BIOMETRIC_ERROR_TIMEOUT + ) + } + manager.getCredentials( + fragmentActivity, + getAuthenticationOptions(), + callback + ) + // Should fail because of timeout + verify(callback).onFailure( + exceptionCaptor.capture() + ) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat( + exception.message, + Is.`is`("Failed to authenticate because the operation timed out.") ) - val retrievedCredentials = credentialsCaptor.firstValue - MatcherAssert.assertThat(retrievedCredentials, Is.`is`(Matchers.notNullValue())) - MatcherAssert.assertThat(retrievedCredentials.accessToken, Is.`is`("accessToken")) - MatcherAssert.assertThat(retrievedCredentials.idToken, Is.`is`("idToken")) - MatcherAssert.assertThat(retrievedCredentials.refreshToken, Is.`is`(Matchers.nullValue())) - MatcherAssert.assertThat(retrievedCredentials.type, Is.`is`("type")) - MatcherAssert.assertThat(retrievedCredentials.expiresAt, Is.`is`(Matchers.notNullValue())) - MatcherAssert.assertThat(retrievedCredentials.expiresAt.time, Is.`is`(expiresAt.time)) - MatcherAssert.assertThat(retrievedCredentials.scope, Is.`is`("scope")) - - // A second call to checkAuthenticationResult should fail as callback is set to null - val retryCheck = manager.checkAuthenticationResult(123, Activity.RESULT_OK) - MatcherAssert.assertThat(retryCheck, Is.`is`(false)) } @Test - public fun shouldGetCredentialsAfterAuthentication() { - val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) - insertTestCredentials(true, true, false, expiresAt, "scope") - Mockito.`when`(storage.retrieveLong("com.auth0.credentials_expires_at")) - .thenReturn(expiresAt.time) - - // Activity is "created" - val activity = Mockito.spy( - Robolectric.buildActivity( - Activity::class.java - ).create().get() - ) - val kService = mock() - Mockito.`when`(activity.getSystemService(Context.KEYGUARD_SERVICE)).thenReturn(kService) - Mockito.`when`(kService.isKeyguardSecure).thenReturn(true) - val confirmCredentialsIntent = mock() - Mockito.`when`(kService.createConfirmDeviceCredentialIntent("theTitle", "theDescription")) - .thenReturn(confirmCredentialsIntent) - // Require authentication - val willRequireAuthentication = - manager.requireAuthentication(activity, 123, "theTitle", "theDescription") - MatcherAssert.assertThat(willRequireAuthentication, Is.`is`(true)) - manager.getCredentials(callback) - val intentCaptor = ArgumentCaptor.forClass(Intent::class.java) - verify(activity) - .startActivityForResult(intentCaptor.capture(), eq(123)) - MatcherAssert.assertThat(intentCaptor.value, Is.`is`(confirmCredentialsIntent)) - - // Continue after successful authentication - val processed = manager.checkAuthenticationResult(123, Activity.RESULT_OK) - MatcherAssert.assertThat(processed, Is.`is`(true)) - verify(callback).onSuccess( - credentialsCaptor.capture() + public fun shouldNotGetCredentialsWhenBiometricsFailedDueToNoSpaceOnDevice() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onFailure( + CredentialsManagerException.BIOMETRIC_ERROR_NO_SPACE + ) + } + manager.getCredentials( + fragmentActivity, + getAuthenticationOptions(), + callback + ) + // Should fail because of no space + verify(callback).onFailure( + exceptionCaptor.capture() + ) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat( + exception.message, + Is.`is`("Failed to authenticate because there is not enough storage remaining on the device.") ) - val retrievedCredentials = credentialsCaptor.firstValue - MatcherAssert.assertThat(retrievedCredentials, Is.`is`(Matchers.notNullValue())) - MatcherAssert.assertThat(retrievedCredentials.accessToken, Is.`is`("accessToken")) - MatcherAssert.assertThat(retrievedCredentials.idToken, Is.`is`("idToken")) - MatcherAssert.assertThat(retrievedCredentials.refreshToken, Is.`is`(Matchers.nullValue())) - MatcherAssert.assertThat(retrievedCredentials.type, Is.`is`("type")) - MatcherAssert.assertThat(retrievedCredentials.expiresAt, Is.`is`(Matchers.notNullValue())) - MatcherAssert.assertThat(retrievedCredentials.expiresAt.time, Is.`is`(expiresAt.time)) - MatcherAssert.assertThat(retrievedCredentials.scope, Is.`is`("scope")) - - // A second call to checkAuthenticationResult should fail as callback is set to null - val retryCheck = manager.checkAuthenticationResult(123, Activity.RESULT_OK) - MatcherAssert.assertThat(retryCheck, Is.`is`(false)) } @Test - public fun shouldNotGetCredentialsWhenCredentialsHaveExpired() { - val credentialsExpiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) - val storedExpiresAt = Date(CredentialsMock.CURRENT_TIME_MS - ONE_HOUR_SECONDS * 1000) - insertTestCredentials(true, true, false, credentialsExpiresAt, "scope") - Mockito.`when`(storage.retrieveLong("com.auth0.credentials_access_token_expires_at")) - .thenReturn(storedExpiresAt.time) - - // Activity is "created" - val activity = Mockito.spy( - Robolectric.buildActivity( - Activity::class.java - ).create().get() - ) - val kService = mock() - Mockito.`when`(activity.getSystemService(Context.KEYGUARD_SERVICE)).thenReturn(kService) - Mockito.`when`(kService.isKeyguardSecure).thenReturn(true) - val confirmCredentialsIntent = mock() - Mockito.`when`(kService.createConfirmDeviceCredentialIntent("theTitle", "theDescription")) - .thenReturn(confirmCredentialsIntent) - // Require authentication - val willRequireAuthentication = - manager.requireAuthentication(activity, 123, "theTitle", "theDescription") - MatcherAssert.assertThat(willRequireAuthentication, Is.`is`(true)) - manager.getCredentials(callback) - - // Should fail because of expired credentials + public fun shouldNotGetCredentialsWhenBiometricsFailedDueToCancellation() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onFailure( + CredentialsManagerException.BIOMETRIC_ERROR_CANCELED + ) + } + manager.getCredentials( + fragmentActivity, + getAuthenticationOptions(), + callback + ) + // Should fail because of cancellation verify(callback).onFailure( exceptionCaptor.capture() ) val exception = exceptionCaptor.firstValue MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) - MatcherAssert.assertThat(exception.message, Is.`is`("No Credentials were previously set.")) - - // A second call to checkAuthenticationResult should fail as callback is set to null - val retryCheck = manager.checkAuthenticationResult(123, Activity.RESULT_OK) - MatcherAssert.assertThat(retryCheck, Is.`is`(false)) + MatcherAssert.assertThat( + exception.message, + Is.`is`("Failed to authenticate because the operation was canceled as the biometric sensor is unavailable, this may happen when the user is switched, the device is locked.") + ) } @Test - public fun shouldNotGetCredentialsWhenCredentialsWereClearedBeforeContinuing() { - val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) - insertTestCredentials(true, true, false, expiresAt, "scope") - Mockito.`when`(storage.retrieveLong("com.auth0.credentials_expires_at")) - .thenReturn(expiresAt.time) + public fun shouldNotGetCredentialsWhenBiometricsFailedDueToLockOut() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onFailure( + CredentialsManagerException.BIOMETRIC_ERROR_LOCKOUT + ) + } + manager.getCredentials( + fragmentActivity, + getAuthenticationOptions(), + callback + ) + // Should fail because of lockout + verify(callback).onFailure( + exceptionCaptor.capture() + ) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat( + exception.message, + Is.`is`("Failed to authenticate because the user has been temporarily locked out, this occurs after 5 failed attempts and lasts for 30 seconds.") + ) + } - // Activity is "created" - val activity = Mockito.spy( - Robolectric.buildActivity( - Activity::class.java - ).create().get() - ) - val kService = mock() - Mockito.`when`(activity.getSystemService(Context.KEYGUARD_SERVICE)).thenReturn(kService) - Mockito.`when`(kService.isKeyguardSecure).thenReturn(true) - val confirmCredentialsIntent = mock() - Mockito.`when`(kService.createConfirmDeviceCredentialIntent("theTitle", "theDescription")) - .thenReturn(confirmCredentialsIntent) - // Require authentication - val willRequireAuthentication = - manager.requireAuthentication(activity, 123, "theTitle", "theDescription") - MatcherAssert.assertThat(willRequireAuthentication, Is.`is`(true)) - manager.getCredentials(callback) - val intentCaptor = ArgumentCaptor.forClass(Intent::class.java) - verify(activity) - .startActivityForResult(intentCaptor.capture(), eq(123)) - MatcherAssert.assertThat(intentCaptor.value, Is.`is`(confirmCredentialsIntent)) + @Test + public fun shouldNotGetCredentialsWhenBiometricsFailedDueToVendorError() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onFailure( + CredentialsManagerException.BIOMETRIC_ERROR_VENDOR + ) + } + manager.getCredentials( + fragmentActivity, + getAuthenticationOptions(), + callback + ) + // Should fail because of vendor error + verify(callback).onFailure( + exceptionCaptor.capture() + ) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat( + exception.message, + Is.`is`("Failed to authenticate because of a vendor-specific error.") + ) + } - // Return null for Credentials JSON since it is cleared - Mockito.`when`(storage.retrieveString("com.auth0.credentials")).thenReturn(null) - val processed = manager.checkAuthenticationResult(123, Activity.RESULT_OK) - MatcherAssert.assertThat(processed, Is.`is`(true)) + @Test + public fun shouldNotGetCredentialsWhenBiometricsFailedDueToPermanentLockout() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onFailure( + CredentialsManagerException.BIOMETRIC_ERROR_LOCKOUT_PERMANENT + ) + } + manager.getCredentials( + fragmentActivity, + getAuthenticationOptions(), + callback + ) + // Should fail because of permanent lockout verify(callback).onFailure( exceptionCaptor.capture() ) val exception = exceptionCaptor.firstValue MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) - MatcherAssert.assertThat(exception.message, Is.`is`("No Credentials were previously set.")) + MatcherAssert.assertThat( + exception.message, + Is.`is`("Failed to authenticate because the user has been permanently locked out.") + ) + } - // A second call to checkAuthenticationResult should fail as callback is set to null - val retryCheck = manager.checkAuthenticationResult(123, Activity.RESULT_OK) - MatcherAssert.assertThat(retryCheck, Is.`is`(false)) + @Test + public fun shouldNotGetCredentialsWhenBiometricsFailedDueToCancellationByUser() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onFailure( + CredentialsManagerException.BIOMETRIC_ERROR_USER_CANCELED + ) + } + manager.getCredentials( + fragmentActivity, + getAuthenticationOptions(), + callback + ) + // Should fail because of cancellation by user + verify(callback).onFailure( + exceptionCaptor.capture() + ) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat( + exception.message, + Is.`is`("Failed to authenticate because the user canceled the operation.") + ) } @Test - public fun shouldNotGetCredentialsAfterCanceledAuthentication() { - val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) - insertTestCredentials(true, true, false, expiresAt, "scope") + public fun shouldNotGetCredentialsWhenBiometricsFailedDueToNoBiometrics() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onFailure( + CredentialsManagerException.BIOMETRIC_ERROR_NO_BIOMETRICS + ) + } + manager.getCredentials( + fragmentActivity, + getAuthenticationOptions(), + callback + ) + // Should fail because of no biometrics + verify(callback).onFailure( + exceptionCaptor.capture() + ) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat( + exception.message, + Is.`is`("Failed to authenticate because the user does not have any biometrics enrolled.") + ) + } - // Activity is "created" - val activity = Mockito.spy( - Robolectric.buildActivity( - Activity::class.java - ).create().get() - ) - val kService = mock() - Mockito.`when`(activity.getSystemService(Context.KEYGUARD_SERVICE)).thenReturn(kService) - Mockito.`when`(kService.isKeyguardSecure).thenReturn(true) - val confirmCredentialsIntent = mock() - Mockito.`when`(kService.createConfirmDeviceCredentialIntent("theTitle", "theDescription")) - .thenReturn(confirmCredentialsIntent) - // Require authentication - val willRequireAuthentication = - manager.requireAuthentication(activity, 123, "theTitle", "theDescription") - MatcherAssert.assertThat(willRequireAuthentication, Is.`is`(true)) - manager.getCredentials(callback) - val intentCaptor = ArgumentCaptor.forClass(Intent::class.java) - verify(activity) - .startActivityForResult(intentCaptor.capture(), eq(123)) - MatcherAssert.assertThat(intentCaptor.value, Is.`is`(confirmCredentialsIntent)) - - // Continue after canceled authentication - val processed = manager.checkAuthenticationResult(123, Activity.RESULT_CANCELED) - MatcherAssert.assertThat(processed, Is.`is`(true)) - verify(callback, never()).onSuccess(any()) + @Test + public fun shouldNotGetCredentialsWhenBiometricsFailedDueToHardwareNotPresent() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onFailure( + CredentialsManagerException.BIOMETRIC_ERROR_HW_NOT_PRESENT + ) + } + manager.getCredentials( + fragmentActivity, + getAuthenticationOptions(), + callback + ) + // Should fail because hardware is not present verify(callback).onFailure( exceptionCaptor.capture() ) - MatcherAssert.assertThat(exceptionCaptor.firstValue, Is.`is`(Matchers.notNullValue())) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) MatcherAssert.assertThat( - exceptionCaptor.firstValue.message, - Is.`is`("The user didn't pass the authentication challenge.") + exception.message, + Is.`is`("Failed to authenticate because the device does not have the required authentication hardware.") ) } @Test - public fun shouldNotGetCredentialsOnDifferentAuthenticationRequestCode() { - val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) - insertTestCredentials(true, true, false, expiresAt, "scope") + public fun shouldNotGetCredentialsWhenBiometricsFailedBecauseUserPressedTheNegativeButton() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onFailure( + CredentialsManagerException.BIOMETRIC_ERROR_NEGATIVE_BUTTON + ) + } + manager.getCredentials( + fragmentActivity, + getAuthenticationOptions(), + callback + ) + // Should fail because user pressed the negative button + verify(callback).onFailure( + exceptionCaptor.capture() + ) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat( + exception.message, + Is.`is`("Failed to authenticate as the user pressed the negative button.") + ) + } - // Activity is "created" - val activity = Mockito.spy( - Robolectric.buildActivity( - Activity::class.java - ).create().get() - ) - val kService = mock() - Mockito.`when`(activity.getSystemService(Context.KEYGUARD_SERVICE)).thenReturn(kService) - Mockito.`when`(kService.isKeyguardSecure).thenReturn(true) - val confirmCredentialsIntent = mock() - Mockito.`when`(kService.createConfirmDeviceCredentialIntent("theTitle", "theDescription")) - .thenReturn(confirmCredentialsIntent) - // Require authentication - val willRequireAuthentication = - manager.requireAuthentication(activity, 100, "theTitle", "theDescription") - MatcherAssert.assertThat(willRequireAuthentication, Is.`is`(true)) - manager.getCredentials(callback) - val intentCaptor = ArgumentCaptor.forClass(Intent::class.java) - verify(activity) - .startActivityForResult(intentCaptor.capture(), eq(100)) - MatcherAssert.assertThat(intentCaptor.value, Is.`is`(confirmCredentialsIntent)) - - // Continue after successful authentication - verifyNoMoreInteractions(callback) - val processed = manager.checkAuthenticationResult(123, Activity.RESULT_OK) - MatcherAssert.assertThat(processed, Is.`is`(false)) + @Test + public fun shouldNotGetCredentialsWhenBiometricsFailedBecauseNoDeviceCredentials() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onFailure( + CredentialsManagerException.BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL + ) + } + manager.getCredentials( + fragmentActivity, + getAuthenticationOptions(), + callback + ) + // Should fail because no device credentials + verify(callback).onFailure( + exceptionCaptor.capture() + ) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat( + exception.message, + Is.`is`("Failed to authenticate because the device does not have pin, pattern, or password setup.") + ) } /* @@ -1729,7 +1688,7 @@ public class SecureCredentialsManagerTest { */ @Test public fun shouldUseCustomClock() { - val manager = SecureCredentialsManager(client, storage, crypto, jwtDecoder) { } + val manager = SecureCredentialsManager(client, storage, crypto, jwtDecoder, serialExecutor, factory) { } val expirationTime = CredentialsMock.CURRENT_TIME_MS //Same as current time --> expired Mockito.`when`(storage.retrieveLong("com.auth0.credentials_access_token_expires_at")) .thenReturn(expirationTime) @@ -1750,7 +1709,7 @@ public class SecureCredentialsManagerTest { @Test(expected = java.lang.IllegalArgumentException::class) public fun shouldUseCustomExecutorForGetCredentials() { - val manager = SecureCredentialsManager(client, storage, crypto, jwtDecoder) { + val manager = SecureCredentialsManager(apiClient = client, storage = storage, crypto = crypto, jwtDecoder = jwtDecoder) { throw java.lang.IllegalArgumentException("Proper Executor Set") } val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS @@ -1759,8 +1718,8 @@ public class SecureCredentialsManagerTest { Mockito.`when`(storage.retrieveString("com.auth0.credentials")) .thenReturn("{\"access_token\":\"accessToken\"}") manager.getCredentials(object : Callback { - override fun onSuccess(result: Credentials) { } - override fun onFailure(error: CredentialsManagerException) { } + override fun onSuccess(result: Credentials) {} + override fun onFailure(error: CredentialsManagerException) {} }) } @@ -1793,10 +1752,12 @@ public class SecureCredentialsManagerTest { Mockito.`when`(crypto.encrypt(expectedJson.toByteArray())) .thenReturn(expectedJson.toByteArray()) manager.getCredentials( - scope = "some changed scope to trigger refresh", - minTtl = 0, - parameters = parameters, - callback = callback + "some changed scope to trigger refresh", + 0, + parameters, + emptyMap(), + false, + callback ) verify(request).addParameters(parameters) @@ -1835,6 +1796,7 @@ public class SecureCredentialsManagerTest { scope = "scope", minTtl = 0, parameters = parameters, + headers = emptyMap(), forceRefresh = true, callback = callback ) @@ -1842,12 +1804,21 @@ public class SecureCredentialsManagerTest { verify(callback).onSuccess(credentialsCaptor.capture()) val retrievedCredentials = credentialsCaptor.firstValue MatcherAssert.assertThat(retrievedCredentials, Is.`is`(Matchers.notNullValue())) - MatcherAssert.assertThat(retrievedCredentials.accessToken, Is.`is`(expectedCredentials.accessToken)) + MatcherAssert.assertThat( + retrievedCredentials.accessToken, + Is.`is`(expectedCredentials.accessToken) + ) MatcherAssert.assertThat(retrievedCredentials.idToken, Is.`is`(expectedCredentials.idToken)) - MatcherAssert.assertThat(retrievedCredentials.refreshToken, Is.`is`(expectedCredentials.refreshToken)) + MatcherAssert.assertThat( + retrievedCredentials.refreshToken, + Is.`is`(expectedCredentials.refreshToken) + ) MatcherAssert.assertThat(retrievedCredentials.type, Is.`is`(expectedCredentials.type)) MatcherAssert.assertThat(retrievedCredentials.expiresAt, Is.`is`(Matchers.notNullValue())) - MatcherAssert.assertThat(retrievedCredentials.expiresAt.time, Is.`is`(expectedCredentials.expiresAt.time)) + MatcherAssert.assertThat( + retrievedCredentials.expiresAt.time, + Is.`is`(expectedCredentials.expiresAt.time) + ) MatcherAssert.assertThat(retrievedCredentials.scope, Is.`is`(expectedCredentials.scope)) } @@ -1856,9 +1827,11 @@ public class SecureCredentialsManagerTest { verifyNoMoreInteractions(client) val expiresAt = Date(CredentialsMock.CURRENT_TIME_MS + ONE_HOUR_SECONDS * 1000) insertTestCredentials(true, true, true, expiresAt, "scope") - manager.getCredentials("scope", + manager.getCredentials( + "scope", 0, emptyMap(), + emptyMap(), false, callback ) @@ -1877,9 +1850,12 @@ public class SecureCredentialsManagerTest { } @Test - public fun shouldBeMarkedSynchronous(){ + public fun shouldBeMarkedSynchronous() { val method = - SecureCredentialsManager::class.java.getMethod("saveCredentials", Credentials::class.java) + SecureCredentialsManager::class.java.getMethod( + "saveCredentials", + Credentials::class.java + ) assertTrue(Modifier.isSynchronized(method.modifiers)) } @@ -1927,4 +1903,25 @@ public class SecureCredentialsManagerTest { private const val ONE_HOUR_SECONDS = (60 * 60).toLong() private const val KEY_ALIAS = "com.auth0.key" } + + private fun getAuthenticationOptions( + title: String = "title", + description: String = "description", + subtitle: String = "subtitle", + negativeButtonText: String = "negativeButtonText", + authenticator: AuthenticationLevel = AuthenticationLevel.STRONG, + enableDeviceCredentialFallback: Boolean = false + ): LocalAuthenticationOptions { + + val builder = LocalAuthenticationOptions.Builder() + builder.apply { + title(title) + subtitle(subtitle) + description(description) + negativeButtonText(negativeButtonText) + authenticator(authenticator) + enableDeviceCredentialFallback(enableDeviceCredentialFallback) + } + return builder.build() + } } \ No newline at end of file From 8c0631882f4f6a95b2cd4eba91a843c28d951950 Mon Sep 17 00:00:00 2001 From: Sai Venkat Desu Date: Mon, 8 Jul 2024 19:42:32 +0530 Subject: [PATCH 14/30] chore: updated sample app to use getCredentials with biometric prompt Signed-off-by: Sai Venkat Desu --- .../com/auth0/sample/DatabaseLoginFragment.kt | 53 +++++++++++++++---- .../res/layout/fragment_database_login.xml | 14 +++++ 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt b/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt index a07729b3..8d9d409f 100644 --- a/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt +++ b/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt @@ -9,6 +9,7 @@ import com.auth0.android.Auth0 import com.auth0.android.authentication.AuthenticationAPIClient import com.auth0.android.authentication.AuthenticationException import com.auth0.android.authentication.storage.AuthenticationLevel +import com.auth0.android.authentication.storage.CredentialsManager import com.auth0.android.authentication.storage.CredentialsManagerException import com.auth0.android.authentication.storage.LocalAuthenticationOptions import com.auth0.android.authentication.storage.SecureCredentialsManager @@ -52,12 +53,23 @@ class DatabaseLoginFragment : Fragment() { AuthenticationAPIClient(account) } - private val credentialsManager: SecureCredentialsManager by lazy { + private val secureCredentialsManager: SecureCredentialsManager by lazy { val storage = SharedPreferencesStorage(requireContext()) val manager = SecureCredentialsManager(requireContext(), authenticationApiClient, storage) manager } + private val credentialsManager: CredentialsManager by lazy { + val storage = SharedPreferencesStorage(requireContext()) + val manager = CredentialsManager(authenticationApiClient, storage) + manager + } + + private val localAuthenticationOptions = + LocalAuthenticationOptions.Builder().title("Biometric").description("description") + .authenticator(AuthenticationLevel.STRONG).negativeButtonText("Cancel").enableDeviceCredentialFallback(true1`) + .build() + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -97,6 +109,9 @@ class DatabaseLoginFragment : Fragment() { binding.btGetCredentials.setOnClickListener { getCreds() } + binding.getCredentialsSecure.setOnClickListener { + getCredsSecure() + } binding.btGetCredentialsAsync.setOnClickListener { launchAsync { getCredsAsync() @@ -172,6 +187,7 @@ class DatabaseLoginFragment : Fragment() { .start(requireContext(), object : Callback { override fun onSuccess(result: Credentials) { credentialsManager.saveCredentials(result) + secureCredentialsManager.saveCredentials(result) Snackbar.make( requireView(), "Hello ${result.user.name}", @@ -250,13 +266,7 @@ class DatabaseLoginFragment : Fragment() { } private fun getCreds() { - val localAuthenticationOptions = - LocalAuthenticationOptions.Builder().title("Biometric").description("description") - .authenticator(AuthenticationLevel.STRONG).negativeButtonText("Cancel") - .build() - credentialsManager.getCredentialsWithAuthentication( - requireActivity(), - localAuthenticationOptions, + credentialsManager.getCredentials( null, 300, emptyMap(), @@ -273,16 +283,37 @@ class DatabaseLoginFragment : Fragment() { override fun onFailure(error: CredentialsManagerException) { Snackbar.make(requireView(), "${error.message}", Snackbar.LENGTH_LONG).show() + } + }) + } + private fun getCredsSecure() { + secureCredentialsManager.getCredentials( + requireActivity(), + localAuthenticationOptions, + object : + Callback { + override fun onSuccess(result: Credentials) { + Snackbar.make( + requireView(), + "Got credentials - ${result.accessToken}", + Snackbar.LENGTH_LONG + ).show() + } + + override fun onFailure(error: CredentialsManagerException) { + Snackbar.make(requireView(), "${error.message}", Snackbar.LENGTH_LONG).show() when (error) { CredentialsManagerException.NO_CREDENTIALS -> { // handle no credentials scenario println("NO_CREDENTIALS: $error") } + CredentialsManagerException.NO_REFRESH_TOKEN -> { // handle no refresh token scenario println("NO_REFRESH_TOKEN: $error") } + CredentialsManagerException.STORE_FAILED -> { // handle store failed scenario println("STORE_FAILED: $error") @@ -290,12 +321,14 @@ class DatabaseLoginFragment : Fragment() { // ... similarly for other error codes } } - }) + } + ) } private suspend fun getCredsAsync() { try { - val credentials = credentialsManager.awaitCredentials() + val credentials = + credentialsManager.awaitCredentials() Snackbar.make( requireView(), "Got credentials - ${credentials.accessToken}", diff --git a/sample/src/main/res/layout/fragment_database_login.xml b/sample/src/main/res/layout/fragment_database_login.xml index a63f1004..af19b8b4 100644 --- a/sample/src/main/res/layout/fragment_database_login.xml +++ b/sample/src/main/res/layout/fragment_database_login.xml @@ -232,5 +232,19 @@ app:layout_constraintEnd_toStartOf="@+id/btWebLogoutAsync" app:layout_constraintTop_toBottomOf="@+id/btWebAuth" /> +