From beaabdffad2edbe1f418b838c306bc20ed3cd774 Mon Sep 17 00:00:00 2001 From: Desu Sai Venkat <48179357+desusai7@users.noreply.github.com> Date: Thu, 1 Aug 2024 21:02:44 +0530 Subject: [PATCH 1/2] feat: implemented biometrics authentication for SecureCredentialsManager using androidx.biometrics package (#745) Signed-off-by: Sai Venkat Desu Signed-off-by: dependabot[bot] Co-authored-by: Igor Wojda Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .snyk | 13 +- EXAMPLES.md | 128 +- README.md | 6 +- V3_MIGRATION_GUIDE.md | 66 + auth0/build.gradle | 7 +- .../src/main/java/com/auth0/android/Auth0.kt | 105 +- .../authentication/AuthenticationAPIClient.kt | 18 +- .../authentication/AuthenticationException.kt | 10 - .../storage/BaseCredentialsManager.kt | 62 +- .../storage/CredentialsManager.kt | 26 +- .../storage/CredentialsManagerException.kt | 188 ++- ...efaultLocalAuthenticationManagerFactory.kt | 16 + .../storage/LocalAuthenticationManager.kt | 134 ++ .../LocalAuthenticationManagerFactory.kt | 12 + .../storage/LocalAuthenticationOptions.kt | 52 + .../storage/SecureCredentialsManager.kt | 372 +++-- .../android/management/UsersAPIClient.kt | 10 +- .../auth0/android/request/DefaultClient.kt | 10 +- .../java/com/auth0/android/request/Request.kt | 8 +- .../request/internal/ResponseUtils.java | 19 - .../android/request/internal/ResponseUtils.kt | 29 + .../com/auth0/android/result/Credentials.kt | 19 +- .../java/com/auth0/android/Auth0Test.java | 62 +- .../AuthenticationAPIClientTest.kt | 10 +- .../AuthenticationExceptionTest.kt | 2 +- .../storage/CredentialsManagerTest.kt | 47 +- .../storage/LocalAuthenticationManagerTest.kt | 606 ++++++++ .../storage/SecureCredentialsManagerTest.kt | 1229 ++++++++++------- .../android/management/UsersAPIClientTest.kt | 10 +- .../android/provider/WebAuthProviderTest.kt | 36 +- .../android/request/DefaultClientTest.kt | 1 + .../internal/CredentialsDeserializerMock.kt | 2 +- .../request/internal/CredentialsGsonTest.kt | 4 +- .../request/internal/ResponseUtilsTest.kt | 27 + .../auth0/android/result/CredentialsMock.kt | 23 +- .../auth0/android/result/CredentialsTest.kt | 24 +- .../com/auth0/android/util/SSLTestUtils.kt | 2 + .../com/auth0/sample/DatabaseLoginFragment.kt | 140 +- .../res/layout/fragment_database_login.xml | 14 + 39 files changed, 2579 insertions(+), 970 deletions(-) create mode 100644 V3_MIGRATION_GUIDE.md 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/LocalAuthenticationManager.kt create mode 100644 auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationManagerFactory.kt create mode 100644 auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationOptions.kt delete mode 100644 auth0/src/main/java/com/auth0/android/request/internal/ResponseUtils.java create mode 100644 auth0/src/main/java/com/auth0/android/request/internal/ResponseUtils.kt create mode 100644 auth0/src/test/java/com/auth0/android/authentication/storage/LocalAuthenticationManagerTest.kt create mode 100644 auth0/src/test/java/com/auth0/android/request/internal/ResponseUtilsTest.kt diff --git a/.snyk b/.snyk index 1935e798b..0524dbc66 100644 --- a/.snyk +++ b/.snyk @@ -5,11 +5,16 @@ ignore: SNYK-JAVA-COMFASTERXMLWOODSTOX-3091135: - '*': reason: Latest version of dokka has this vulnerability - expires: 2024-06-27T07:00:56.333Z - created: 2024-05-28T07:00:56.334Z + expires: 2024-08-31T12:08:37.765Z + created: 2024-08-01T12:08:37.770Z SNYK-JAVA-ORGJETBRAINSKOTLIN-2393744: - '*': reason: Latest version of dokka has this vulnerability - expires: 2024-06-27T07:01:24.820Z - created: 2024-05-28T07:01:24.825Z + expires: 2024-08-31T12:08:55.924Z + created: 2024-08-01T12:08:55.927Z + SNYK-JAVA-COMFASTERXMLJACKSONCORE-7569538: + - '*': + reason: Latest version of dokka has this vulnerability + expires: 2024-08-31T12:08:02.966Z + created: 2024-08-01T12:08:02.973Z patch: {} diff --git a/EXAMPLES.md b/EXAMPLES.md index a0784dcbf..78c595146 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -501,18 +501,16 @@ This version adds encryption to the data storage. Additionally, in those devices The usage is similar to the previous version, with the slight difference that the manager now requires a valid android `Context` as shown below: ```kotlin -val authentication = AuthenticationAPIClient(account) val storage = SharedPreferencesStorage(this) -val manager = SecureCredentialsManager(this, authentication, storage) +val manager = SecureCredentialsManager(this, account, storage) ```
Using Java ```java -AuthenticationAPIClient authentication = new AuthenticationAPIClient(account); Storage storage = new SharedPreferencesStorage(this); -SecureCredentialsManager manager = new SecureCredentialsManager(this, authentication, storage); +SecureCredentialsManager manager = new SecureCredentialsManager(this, account, storage); ```
@@ -520,55 +518,64 @@ SecureCredentialsManager manager = new SecureCredentialsManager(this, authentica You can require the user authentication to obtain credentials. This will make the manager prompt the user with the device's configured Lock Screen, which they must pass correctly in order to obtain the credentials. **This feature is only available on devices where the user has setup a secured Lock Screen** (PIN, Pattern, Password or Fingerprint). -To enable authentication you must call the `requireAuthentication` method passing a valid _Activity_ context, a request code that represents the authentication call, and the title and description to display in the Lock Screen. As seen in the snippet below, you can leave these last two parameters with `null` to use the system's default title and description. It's only safe to call this method before the Activity is started. +To enable authentication you must supply an instance of `FragmentActivity` on which the authentication prompt to be shown, and an instance of `LocalAuthenticationOptions` to configure the authentication prompt with details like title and authentication level when creating an instance of `SecureCredentialsManager` as shown in the snippet below. ```kotlin -//You might want to define a constant with the Request Code -companion object { - const val AUTH_REQ_CODE = 111 -} - -manager.requireAuthentication(this, AUTH_REQ_CODE, null, null) +val localAuthenticationOptions = + LocalAuthenticationOptions.Builder().setTitle("Authenticate").setDescription("Accessing Credentials") + .setAuthenticationLevel(AuthenticationLevel.STRONG).setNegativeButtonText("Cancel") + .setDeviceCredentialFallback(true) + .build() +val storage = SharedPreferencesStorage(this) +val manager = SecureCredentialsManager( + this, account, storage, fragmentActivity, + localAuthenticationOptions +) ```
Using Java ```java -//You might want to define a constant with the Request Code -private static final int AUTH_REQ_CODE = 11; - -manager.requireAuthentication(this, AUTH_REQ_CODE, null, null); +LocalAuthenticationOptions localAuthenticationOptions = + new LocalAuthenticationOptions.Builder().setTitle("Authenticate").setDescription("Accessing Credentials") + .setAuthenticationLevel(AuthenticationLevel.STRONG).setNegativeButtonText("Cancel") + .setDeviceCredentialFallback(true) + .build(); +Storage storage = new SharedPreferencesStorage(context); +SecureCredentialsManager secureCredentialsManager = new SecureCredentialsManager( + context, auth0, storage, fragmentActivity, + localAuthenticationOptions); ```
-When the above conditions are met and the manager requires the user authentication, it will use the activity context to launch the Lock Screen activity and wait for its result. If your activity is a subclass of `ComponentActivity`, this will be handled automatically for you internally. Otherwise, your activity must override the `onActivityResult` method and pass the request code and result code to the manager's `checkAuthenticationResult` method to verify if this request was successful or not. +**Points to be Noted**: -```kotlin - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (manager.checkAuthenticationResult(requestCode, resultCode)) { - return - } - super.onActivityResult(requestCode, resultCode, data) -} -``` +On Android API 29 and below, specifying **DEVICE_CREDENTIAL** alone as the authentication level is not supported. +On Android API 28 and 29, specifying **STRONG** as the authentication level along with enabling device credential fallback is not supported. -
- Using Java -```java -@Override -protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (manager.checkAuthenticationResult(requestCode, resultCode)) { - return; - } - super.onActivityResult(requestCode, resultCode, data); -} -``` -
+#### Creating LocalAuthenticationOptions object for requiring Authentication while using SecureCredentialsManager + +`LocalAuthenticationOptions` class exposes a Builder class to create an instance of it. Details about the methods are explained below: + +- **setTitle(title: String): Builder** - Sets the title to be displayed in the Authentication Prompt. +- **setSubTitle(subtitle: String?): Builder** - Sets the subtitle of the Authentication Prompt. +- **setDescription(description: String?): Builder** - Sets the description for the Authentication Prompt. +- **setAuthenticationLevel(authenticationLevel: AuthenticationLevel): Builder** - Sets the authentication level, more on this can be found [here](#authenticationlevel-enum-values) +- **setDeviceCredentialFallback(enableDeviceCredentialFallback: Boolean): Builder** - Enables/disables device credential fallback. +- **setNegativeButtonText(negativeButtonText: String): Builder** - Sets the negative button text, used only when the device credential fallback is disabled (or) the authentication level is not set to `AuthenticationLevel.DEVICE_CREDENTIAL`. +- **build(): LocalAuthenticationOptions** - Constructs the LocalAuthenticationOptions instance. + -If the manager consumed the event, it will return true and later invoke the callback's `onSuccess` with the decrypted credentials. +#### AuthenticationLevel Enum Values +AuthenticationLevel is an enum that defines the different levels of authentication strength required for local authentication mechanisms. + +**Enum Values**: +- **STRONG**: Any biometric (e.g., fingerprint, iris, or face) on the device that meets or exceeds the requirements for Class 3 (formerly Strong). +- **WEAK**: Any biometric (e.g., fingerprint, iris, or face) on the device that meets or exceeds the requirements for Class 2 (formerly Weak), as defined by the Android CDD. +- **DEVICE_CREDENTIAL**: The non-biometric credential used to secure the device (i.e., PIN, pattern, or password). ### Handling Credentials Manager exceptions @@ -579,6 +586,27 @@ In the event that something happened while trying to save or retrieve the creden - Device's Lock Screen security settings have changed (e.g. the PIN code was changed). Even when `hasCredentials` returns true, the encryption keys will be deemed invalid and until `saveCredentials` is called again it won't be possible to decrypt any previously existing content, since they keys used back then are not the same as the new ones. - Device is not compatible with some of the algorithms required by the `SecureCredentialsManager` class. This is considered a catastrophic event and might happen when the OEM has modified the Android ROM removing some of the officially included algorithms. Nevertheless, it can be checked in the exception instance itself by calling `isDeviceIncompatible`. By doing so you can decide the fallback for storing the credentials, such as using the regular `CredentialsManager`. +You can access the `code` property of the `CredentialsManagerException` to understand why the operation with `CredentialsManager` has failed and the `message` property of the `CredentialsManagerException` would give you a description of the exception. + +Starting from version `3.0.0` you can even pass the exception to a `when` expression and handle the exception accordingly in your app's logic as shown in the below code snippet: + +```kotlin +when(credentialsManagerException) { + CredentialsManagerException.NO_CREDENTIALS - > { + // handle no credentials scenario + } + + CredentialsManagerException.NO_REFRESH_TOKEN - > { + // handle no refresh token scenario + } + + CredentialsManagerException.STORE_FAILED - > { + // handle store failed scenario + } + // ... similarly for other error codes +} +``` + ## Bot Protection If you are using the [Bot Protection](https://auth0.com/docs/anomaly-detection/bot-protection) feature and performing database login/signup via the Authentication API, you need to handle the `AuthenticationException#isVerificationRequired()` error. It indicates that the request was flagged as suspicious and an additional verification step is necessary to log the user in. That verification step is web-based, so you need to use Universal Login to complete it. @@ -698,7 +726,7 @@ val users = UsersAPIClient(account, "api access token") Using Java ```java -Auth0 account = new Auth0("client id", "domain"); +Auth0 account = Auth0.getInstance("client id", "domain"); UsersAPIClient users = new UsersAPIClient(account, "api token"); ``` @@ -918,7 +946,7 @@ If you are a user of Auth0 Private Cloud with ["Custom Domains"](https://auth0.c The validation is done automatically for Web Authentication ```kotlin -val account = Auth0("{YOUR_CLIENT_ID}", "{YOUR_CUSTOM_DOMAIN}") +val account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_CUSTOM_DOMAIN}") WebAuthProvider.login(account) .withIdTokenVerificationIssuer("https://{YOUR_AUTH0_DOMAIN}/") @@ -928,7 +956,7 @@ WebAuthProvider.login(account) For Authentication Client, the method `validateClaims()` has to be called to enable it. ```kotlin -val auth0 = Auth0("YOUR_CLIENT_ID", "YOUR_DOMAIN") +val auth0 = Auth0.getInstance("YOUR_CLIENT_ID", "YOUR_DOMAIN") val client = AuthenticationAPIClient(auth0) client .login("{username or email}", "{password}", "{database connection name}") @@ -944,7 +972,7 @@ client Using coroutines ```kotlin -val auth0 = Auth0("YOUR_CLIENT_ID", "YOUR_DOMAIN") +val auth0 = Auth0.getInstance("YOUR_CLIENT_ID", "YOUR_DOMAIN") val client = AuthenticationAPIClient(auth0) try { @@ -964,7 +992,7 @@ try { Using Java ```java -Auth0 auth0 = new Auth0("client id", "domain"); +Auth0 auth0 = Auth0.getInstance("client id", "domain"); AuthenticationAPIClient client = new AuthenticationAPIClient(account); client .login("{username or email}", "{password}", "{database connection name}") @@ -1039,7 +1067,7 @@ val netClient = DefaultClient( readTimeout = 30 ) -val account = Auth0("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}") +val account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}") account.networkingClient = netClient ``` @@ -1051,7 +1079,7 @@ DefaultClient netClient = new DefaultClient( connectTimeout = 30, readTimeout = 30 ); -Auth0 account = new Auth0("client id", "domain"); +Auth0 account = Auth0.getInstance("client id", "domain"); account.networkingClient = netClient; ``` @@ -1063,7 +1091,7 @@ val netClient = DefaultClient( enableLogging = true ) -val account = Auth0("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}") +val account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}") account.networkingClient = netClient ``` @@ -1074,7 +1102,7 @@ account.networkingClient = netClient DefaultClient netClient = new DefaultClient( enableLogging = true ); -Auth0 account = new Auth0("client id", "domain"); +Auth0 account = Auth0.getInstance("client id", "domain"); account.networkingClient = netClient; ``` @@ -1086,7 +1114,7 @@ val netClient = DefaultClient( defaultHeaders = mapOf("{HEADER-NAME}" to "{HEADER-VALUE}") ) -val account = Auth0("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}") +val account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}") account.networkingClient = netClient ``` @@ -1100,7 +1128,7 @@ defaultHeaders.put("{HEADER-NAME}", "{HEADER-VALUE}"); DefaultClient netClient = new DefaultClient( defaultHeaders = defaultHeaders ); -Auth0 account = new Auth0("client id", "domain"); +Auth0 account = Auth0.getInstance("client id", "domain"); account.networkingClient = netClient; ``` @@ -1120,7 +1148,7 @@ class CustomNetClient : NetworkingClient { } } -val account = Auth0("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}") +val account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}") account.networkingClient = CustomNetClient() ``` @@ -1139,7 +1167,7 @@ class CustomNetClient extends NetworkingClient { } }; -Auth0 account = new Auth0("client id", "domain"); +Auth0 account = Auth0.getInstance("client id", "domain"); account.networkingClient = new CustomNetClient(); ``` diff --git a/README.md b/README.md index d1eab885c..17cbf124c 100644 --- a/README.md +++ b/README.md @@ -68,14 +68,14 @@ Open your app's `AndroidManifest.xml` file and add the following permission. First, create an instance of `Auth0` with your Application information ```kotlin -val account = Auth0("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}") +val account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}") ```
Using Java ```java -Auth0 account = new Auth0("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}"); +Auth0 account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}"); ```
@@ -94,7 +94,7 @@ Alternatively, you can save your Application information in the `strings.xml` fi You can then create a new Auth0 instance by passing an Android Context: ```kotlin -val account = Auth0(context) +val account = Auth0.getInstance(context) ``` diff --git a/V3_MIGRATION_GUIDE.md b/V3_MIGRATION_GUIDE.md new file mode 100644 index 000000000..3f9f12383 --- /dev/null +++ b/V3_MIGRATION_GUIDE.md @@ -0,0 +1,66 @@ +# Migration Guide from SDK v2 to v3 + +## Breaking Changes + +### Auth0 Class +- **Constructor**: The constructor of the `Auth0` class is now private. Use `Auth0.getInstance(clientId, domain)` to get an instance. This method checks if an instance with the given configuration exists; if yes, it returns it, otherwise, it creates a new one. + +### BaseCredentialsManager Interface +- **New Methods**: Added multiple overloads of `getCredentials()` and `awaitCredentials()` to the `BaseCredentialsManager` interface. All implementations of this interface must now override these new methods. + +### Request Interface +- **await Function**: The `await` function of the `Request` interface is now abstract. All implementations must implement this method. + +### Credentials Class +- **Data Class**: The `Credentials` class is now a data class and can no longer be extended. The `currentTimeInMillis` property has been removed. + +### SecureCredentialsManager +- **requireAuthentication Method**: The `requireAuthentication` method, used to enable authentication before obtaining credentials, has been removed. Refer to the [Enabling Authentication](#enabling-authentication-before-obtaining-credentials) section for the new approach. + +## Changes + +### Biometrics Authentication +- **Library Update**: Implementation of biometrics authentication for retrieving credentials securely is now done using the `androidx.biometric.biometric` library. + +### CredentialsManagerException +- **Enum Code**: The `CredentialsManagerException` now contains an enum code. You can use a `when` expression to handle different error scenarios: + +```kotlin +when (credentialsManagerException) { + CredentialsManagerException.NO_CREDENTIALS -> { + // handle no credentials scenario + } + CredentialsManagerException.NO_REFRESH_TOKEN -> { + // handle no refresh token scenario + } + CredentialsManagerException.STORE_FAILED -> { + // handle store failed scenario + } + // ... similarly for other error codes +} +``` + +## Enabling Authentication before Obtaining Credentials + +To enable authentication before obtaining credentials, you need to pass the below to the constructor of `SecureCredentialsManager`: +- An instance of `FragmentActivity` where the authentication prompt should be shown. +- An instance of `LocalAuthenticationOptions` to configure details like the level of authentication (Strong, Weak), prompt title, etc. + +### Example + +```kotlin +private val localAuthenticationOptions = LocalAuthenticationOptions.Builder() + .setTitle("Authenticate to Access Credentials") + .setDescription("description") + .setAuthenticationLevel(AuthenticationLevel.STRONG) + .setDeviceCredentialFallback(true) + .build() + +val storage = SharedPreferencesStorage(context) +val manager = SecureCredentialsManager( + context, account, storage, fragmentActivity, + localAuthenticationOptions +) +``` + +If you need more information, please refer to the [examples.md](examples.md#requiring-authentication) file under the section **Requiring Authentication**. \ No newline at end of file diff --git a/auth0/build.gradle b/auth0/build.gradle index 6b1f8c02b..22897333c 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,6 +92,8 @@ dependencies { implementation 'com.google.code.gson:gson:2.8.9' implementation 'com.google.androidbrowserhelper:androidbrowserhelper:2.4.0' + implementation "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" @@ -102,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/main/java/com/auth0/android/Auth0.kt b/auth0/src/main/java/com/auth0/android/Auth0.kt index 72742f2d6..38cc84838 100755 --- a/auth0/src/main/java/com/auth0/android/Auth0.kt +++ b/auth0/src/main/java/com/auth0/android/Auth0.kt @@ -7,52 +7,50 @@ import com.auth0.android.util.Auth0UserAgent import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import java.util.* +import java.util.concurrent.Executor +import java.util.concurrent.Executors /** * Represents your Auth0 account information (clientId & domain), * and it's used to obtain clients for Auth0's APIs. * * ``` - * val auth0 = Auth0("YOUR_CLIENT_ID", "YOUR_DOMAIN") + * val auth0 = Auth0.getInstance("YOUR_CLIENT_ID", "YOUR_DOMAIN") *``` * * This SDK only supports OIDC-Conformant clients, and will use Auth0's current authentication pipeline. * For more information, please see the [OIDC adoption guide](https://auth0.com/docs/api-auth/tutorials/adoption). * * @param clientId of your Auth0 application - * @param domain of your Auth0 account + * @param domainUrl of your Auth0 account * @param configurationDomain where Auth0's configuration will be fetched, change it if using an on-premise Auth0 server. By default is Auth0 public cloud. */ -public open class Auth0 @JvmOverloads constructor( +public open class Auth0 private constructor( /** * @return your Auth0 application client identifier */ - public val clientId: String, domain: String, configurationDomain: String? = null + public val clientId: String, + private val domainUrl: HttpUrl, + public val configurationDomain: String? = null, ) { - private val domainUrl: HttpUrl? - private val configurationUrl: HttpUrl + public val domain: String = domainUrl.host + private val configurationUrl: HttpUrl = ensureValidUrl(configurationDomain) ?: domainUrl /** * @return Auth0 user agent information sent in every request */ - public var auth0UserAgent: Auth0UserAgent + public var auth0UserAgent: Auth0UserAgent = Auth0UserAgent() /** * The networking client instance used to make HTTP requests. */ public var networkingClient: NetworkingClient = DefaultClient() + /** - * Creates a new Auth0 instance with the 'com_auth0_client_id' and 'com_auth0_domain' values - * defined in the project String resources file. - * If the values are not found, IllegalArgumentException will raise. - * - * @param context a valid context + * The single thread executor used to run tasks in the background throughout this Auth0 instance. */ - public constructor(context: Context) : this( - getResourceFromContext(context, "com_auth0_client_id"), - getResourceFromContext(context, "com_auth0_domain") - ) + public val executor: Executor = Executors.newSingleThreadExecutor() /** * @return your Auth0 account domain url @@ -91,18 +89,57 @@ public open class Auth0 @JvmOverloads constructor( .build() .toString() - private fun ensureValidUrl(url: String?): HttpUrl? { - if (url == null) { - return null + + public companion object { + + private var instance: Auth0? = null + + /** + * Creates a new Auth0 instance with the 'com_auth0_client_id' and 'com_auth0_domain' values + * defined in the project String resources file, if the instance with the same values doesn't exist yet and returns it. + * If it already exists, it will return the existing instance. + * If the values 'com_auth0_client_id' and 'com_auth0_domain' are not found in project String resources file, IllegalArgumentException will raise. + * + * @param context a valid context + */ + @JvmStatic + public fun getInstance(context: Context): Auth0 { + val clientId = getResourceFromContext(context, "com_auth0_client_id") + val domain = getResourceFromContext(context, "com_auth0_domain") + return getInstance(clientId, domain) + } + + /** + * Creates a new Auth0 instance with the given clientId and domain, if it doesn't exist yet and returns it. + * If it already exists, it will return the existing instance. + */ + @JvmStatic + public fun getInstance( + clientId: String, + domain: String + ): Auth0 { + return getInstance(clientId, domain, null) + } + + + /** + * Creates a new Auth0 instance with the given clientId, domain and configurationDomain, if it doesn't exist yet and returns it. + * If it already exists, it will return the existing instance. + */ + @JvmStatic + public fun getInstance( + clientId: String, + domain: String, + configurationDomain: String? + ): Auth0 { + val domainUrl = ensureValidUrl(domain) + requireNotNull(domainUrl) { String.format("Invalid domain url: '%s'", domain) } + if (instance == null || instance!!.clientId != clientId || instance!!.domainUrl.host != domainUrl.host || instance!!.configurationDomain != configurationDomain) { + instance = Auth0(clientId, domainUrl, configurationDomain) + } + return instance!! } - val normalizedUrl = url.lowercase(Locale.ROOT) - require(!normalizedUrl.startsWith("http://")) { "Invalid domain url: '$url'. Only HTTPS domain URLs are supported. If no scheme is passed, HTTPS will be used." } - val safeUrl = - if (normalizedUrl.startsWith("https://")) normalizedUrl else "https://$normalizedUrl" - return safeUrl.toHttpUrlOrNull() - } - private companion object { private fun getResourceFromContext(context: Context, resName: String): String { val stringRes = context.resources.getIdentifier(resName, "string", context.packageName) require(stringRes != 0) { @@ -113,12 +150,16 @@ public open class Auth0 @JvmOverloads constructor( } return context.getString(stringRes) } - } - init { - domainUrl = ensureValidUrl(domain) - requireNotNull(domainUrl) { String.format("Invalid domain url: '%s'", domain) } - configurationUrl = ensureValidUrl(configurationDomain) ?: domainUrl - auth0UserAgent = Auth0UserAgent() + private fun ensureValidUrl(url: String?): HttpUrl? { + if (url == null) { + return null + } + val normalizedUrl = url.lowercase(Locale.ROOT) + require(!normalizedUrl.startsWith("http://")) { "Invalid domain url: '$url'. Only HTTPS domain URLs are supported. If no scheme is passed, HTTPS will be used." } + val safeUrl = + if (normalizedUrl.startsWith("https://")) normalizedUrl else "https://$normalizedUrl" + return safeUrl.toHttpUrlOrNull() + } } } \ No newline at end of file diff --git a/auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt b/auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt index eeb144a13..ecac17918 100755 --- a/auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt @@ -3,10 +3,12 @@ package com.auth0.android.authentication import androidx.annotation.VisibleForTesting import com.auth0.android.Auth0 import com.auth0.android.Auth0Exception +import com.auth0.android.NetworkErrorException import com.auth0.android.request.* import com.auth0.android.request.internal.* import com.auth0.android.request.internal.GsonAdapter.Companion.forMap import com.auth0.android.request.internal.GsonAdapter.Companion.forMapOf +import com.auth0.android.request.internal.ResponseUtils.isNetworkError import com.auth0.android.result.Challenge import com.auth0.android.result.Credentials import com.auth0.android.result.DatabaseUser @@ -20,7 +22,7 @@ import java.security.PublicKey /** * API client for Auth0 Authentication API. * ``` - * val auth0 = Auth0("YOUR_CLIENT_ID", "YOUR_DOMAIN") + * val auth0 = Auth0.getInstance("YOUR_CLIENT_ID", "YOUR_DOMAIN") * val client = AuthenticationAPIClient(auth0) * ``` * @@ -38,7 +40,7 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe * Example usage: * * ``` - * val auth0 = Auth0("YOUR_CLIENT_ID", "YOUR_DOMAIN") + * val auth0 = Auth0.getInstance("YOUR_CLIENT_ID", "YOUR_DOMAIN") * val client = AuthenticationAPIClient(auth0) * ``` * @param auth0 account information @@ -741,7 +743,11 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe val credentialsAdapter: JsonAdapter = GsonAdapter( Credentials::class.java, gson ) - val request = BaseAuthenticationRequest(factory.post(url.toString(), credentialsAdapter), clientId, baseURL) + val request = BaseAuthenticationRequest( + factory.post(url.toString(), credentialsAdapter), + clientId, + baseURL + ) request.addParameters(requestParameters) return request } @@ -811,6 +817,12 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe } override fun fromException(cause: Throwable): AuthenticationException { + if (isNetworkError(cause)) { + return AuthenticationException( + "Failed to execute the network request", + NetworkErrorException(cause) + ) + } return AuthenticationException( "Something went wrong", Auth0Exception("Something went wrong", cause) diff --git a/auth0/src/main/java/com/auth0/android/authentication/AuthenticationException.kt b/auth0/src/main/java/com/auth0/android/authentication/AuthenticationException.kt index 652204db4..c0ec90419 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/AuthenticationException.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/AuthenticationException.kt @@ -4,10 +4,7 @@ import android.text.TextUtils import android.util.Log import com.auth0.android.Auth0Exception import com.auth0.android.NetworkErrorException -import java.net.SocketTimeoutException -import java.net.UnknownHostException import com.auth0.android.provider.TokenValidationException -import java.net.SocketException public class AuthenticationException : Auth0Exception { private var code: String? = null @@ -107,15 +104,8 @@ public class AuthenticationException : Auth0Exception { } // When the request failed due to network issues - // Currently [NetworkErrorException] is not properly thrown from [createErrorAdapter] in - // [AuthenticationAPIClient] and [UserAPIClient]. This will be fixed in the next major to avoid - // breaking change in the current major. We are not using IOException to check for the error - // since it is too broad. public val isNetworkError: Boolean get() = cause is NetworkErrorException - || cause?.cause is UnknownHostException - || cause?.cause is SocketTimeoutException - || cause?.cause is SocketException // When there is no Browser app installed to handle the web authentication public val isBrowserAppNotAvailable: Boolean 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 f3169abe0..506f4d9c3 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 @@ -6,9 +6,6 @@ import com.auth0.android.callback.Callback import com.auth0.android.result.Credentials import com.auth0.android.util.Clock import java.util.* -import java.util.concurrent.Executor -import java.util.concurrent.Executors -import kotlin.math.min /** * Base class meant to abstract common logic across Credentials Manager implementations. @@ -39,6 +36,65 @@ public abstract class BaseCredentialsManager internal constructor( callback: Callback ) + public abstract fun getCredentials( + scope: String?, + minTtl: Int, + parameters: Map, + callback: Callback + ) + + public abstract fun getCredentials( + scope: String?, + minTtl: Int, + parameters: Map, + forceRefresh: Boolean, + callback: Callback + ) + + public abstract fun getCredentials( + scope: String?, + minTtl: Int, + parameters: Map, + headers: Map, + forceRefresh: Boolean, + callback: Callback + ) + + @JvmSynthetic + @Throws(CredentialsManagerException::class) + public abstract suspend fun awaitCredentials(): Credentials + + @JvmSynthetic + @Throws(CredentialsManagerException::class) + public abstract suspend fun awaitCredentials(scope: String?, minTtl: Int): Credentials + + @JvmSynthetic + @Throws(CredentialsManagerException::class) + public abstract suspend fun awaitCredentials( + scope: String?, + minTtl: Int, + parameters: Map + ): Credentials + + @JvmSynthetic + @Throws(CredentialsManagerException::class) + public abstract suspend fun awaitCredentials( + scope: String?, + minTtl: Int, + parameters: Map, + forceRefresh: Boolean + ): Credentials + + @JvmSynthetic + @Throws(CredentialsManagerException::class) + public abstract suspend fun awaitCredentials( + scope: String?, + minTtl: Int, + parameters: Map, + headers: Map, + forceRefresh: Boolean + ): Credentials + public abstract fun clearCredentials() public abstract fun hasValidCredentials(): Boolean public abstract fun hasValidCredentials(minTtl: Long): Boolean 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 2efe85375..9e6af7d9a 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) @@ -61,7 +61,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting */ @JvmSynthetic @Throws(CredentialsManagerException::class) - public suspend fun awaitCredentials(): Credentials { + override suspend fun awaitCredentials(): Credentials { return awaitCredentials(null, 0) } @@ -76,7 +76,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting */ @JvmSynthetic @Throws(CredentialsManagerException::class) - public suspend fun awaitCredentials(scope: String?, minTtl: Int): Credentials { + override suspend fun awaitCredentials(scope: String?, minTtl: Int): Credentials { return awaitCredentials(scope, minTtl, emptyMap()) } @@ -92,7 +92,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting */ @JvmSynthetic @Throws(CredentialsManagerException::class) - public suspend fun awaitCredentials( + override suspend fun awaitCredentials( scope: String?, minTtl: Int, parameters: Map @@ -113,7 +113,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting */ @JvmSynthetic @Throws(CredentialsManagerException::class) - public suspend fun awaitCredentials( + override suspend fun awaitCredentials( scope: String?, minTtl: Int, parameters: Map, @@ -136,7 +136,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting */ @JvmSynthetic @Throws(CredentialsManagerException::class) - public suspend fun awaitCredentials( + override suspend fun awaitCredentials( scope: String?, minTtl: Int, parameters: Map, @@ -200,7 +200,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting * @param parameters additional parameters to send in the request to refresh expired credentials * @param callback the callback that will receive a valid [Credentials] or the [CredentialsManagerException]. */ - public fun getCredentials( + override fun getCredentials( scope: String?, minTtl: Int, parameters: Map, @@ -220,7 +220,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting * @param forceRefresh this will avoid returning the existing credentials and retrieves a new one even if valid credentials exist. * @param callback the callback that will receive a valid [Credentials] or the [CredentialsManagerException]. */ - public fun getCredentials( + override fun getCredentials( scope: String?, minTtl: Int, parameters: Map, @@ -242,7 +242,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting * @param forceRefresh this will avoid returning the existing credentials and retrieves a new one even if valid credentials exist. * @param callback the callback that will receive a valid [Credentials] or the [CredentialsManagerException]. */ - public fun getCredentials( + override fun getCredentials( scope: String?, minTtl: Int, parameters: Map, @@ -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 2858b958b..f90af7a35 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,190 @@ 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, + REVOKE_FAILED, + LARGE_MIN_TTL, + INCOMPATIBLE_DEVICE, + CRYPTO_EXCEPTION, + BIOMETRIC_NO_ACTIVITY, + 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? + + + 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 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) + + // 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 = + 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_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_ACTIVITY: CredentialsManagerException = + CredentialsManagerException(Code.BIOMETRIC_NO_ACTIVITY) + 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." + 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.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. Any previously stored content is now lost. Please try saving the credentials again." + + Code.BIOMETRIC_NO_ACTIVITY -> "Cannot authenticate as the activity passed is null." + 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 -> "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." + } + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is CredentialsManagerException) return false + return code == other.code + } + + override fun hashCode(): Int { + return code.hashCode() + } /** * 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/DefaultLocalAuthenticationManagerFactory.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/DefaultLocalAuthenticationManagerFactory.kt new file mode 100644 index 000000000..6d2d299f5 --- /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/LocalAuthenticationManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationManager.kt new file mode 100644 index 000000000..3b7e81965 --- /dev/null +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationManager.kt @@ -0,0 +1,134 @@ +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.biometric.BiometricPrompt.AuthenticationCallback +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 biometricManager: BiometricManager = BiometricManager.from(activity), + @get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal val resultCallback: Callback, +) : AuthenticationCallback() { + + 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 { + 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) { + resultCallback.onFailure( + generateExceptionFromAuthenticationPossibilityError( + isAuthenticationPossible + ) + ) + return + } + + val bioMetricPromptInfoBuilder = BiometricPrompt.PromptInfo.Builder().apply { + authenticationOptions.run { + setTitle(title) + setSubtitle(subtitle) + setDescription(description) + if (!enableDeviceCredentialFallback && authenticationLevel != AuthenticationLevel.DEVICE_CREDENTIAL) { + setNegativeButtonText(negativeButtonText) + } + } + setAllowedAuthenticators(authenticationLevels) + } + + val biometricPromptInfo = bioMetricPromptInfoBuilder.build() + val biometricPrompt = BiometricPrompt( + activity, + 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( + 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_ERROR_STATUS_UNKNOWN + ) + return exceptionCode[errorCode] + ?: CredentialsManagerException.BIOMETRIC_AUTHENTICATION_CHECK_FAILED + } + + 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 + } + + class UiThreadExecutor : Executor { + private val handler = Handler(Looper.getMainLooper()) + + override fun execute(command: Runnable) { + handler.post(command) + } + } +} \ 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 000000000..e20590ee0 --- /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/LocalAuthenticationOptions.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/LocalAuthenticationOptions.kt new file mode 100644 index 000000000..8f47d204e --- /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 authenticationLevel: AuthenticationLevel = AuthenticationLevel.STRONG, + private var enableDeviceCredentialFallback: Boolean = false, + private var negativeButtonText: String = "Cancel" + ) { + + public fun setTitle(title: String): Builder = apply { this.title = title } + public fun setSubTitle(subtitle: String?): Builder = apply { this.subtitle = subtitle } + public fun setDescription(description: String?): Builder = + apply { this.description = description } + + public fun setAuthenticationLevel(authenticationLevel: AuthenticationLevel): Builder = + apply { this.authenticationLevel = authenticationLevel } + + public fun setDeviceCredentialFallback(enableDeviceCredentialFallback: Boolean): Builder = + apply { this.enableDeviceCredentialFallback = enableDeviceCredentialFallback } + + public fun setNegativeButtonText(negativeButtonText: String): Builder = + apply { this.negativeButtonText = negativeButtonText } + + public fun build(): LocalAuthenticationOptions = LocalAuthenticationOptions( + title ?: throw IllegalArgumentException("Title must be provided"), + subtitle, + description, + authenticationLevel, + 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 de4a1703f..fcbe4aefe 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,32 +1,23 @@ 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.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.annotation.IntRange import androidx.annotation.VisibleForTesting -import androidx.lifecycle.Lifecycle +import androidx.fragment.app.FragmentActivity +import com.auth0.android.Auth0 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 import com.auth0.android.result.OptionalCredentials import com.google.gson.Gson import kotlinx.coroutines.suspendCancellableCoroutine +import java.lang.ref.WeakReference import java.util.* import java.util.concurrent.Executor -import java.util.concurrent.Executors import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @@ -39,120 +30,59 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT storage: Storage, private val crypto: CryptoUtil, jwtDecoder: JWTDecoder, - private val serialExecutor: Executor + private val serialExecutor: Executor, + private val fragmentActivity: WeakReference? = null, + private val localAuthenticationOptions: LocalAuthenticationOptions? = null, + private val localAuthenticationManagerFactory: LocalAuthenticationManagerFactory? = null, ) : BaseCredentialsManager(apiClient, storage, jwtDecoder) { private val gson: Gson = GsonProvider.gson - //Changeable by the user - private var authenticateBeforeDecrypt: Boolean - private var authenticationRequestCode = -1 - private var activity: Activity? = null - private var activityResultContract: ActivityResultLauncher? = 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 /** * Creates a new SecureCredentialsManager to handle Credentials * * @param context a valid context - * @param apiClient the Auth0 Authentication API Client to handle token refreshment when needed. + * @param auth0 the Auth0 account information to use * @param storage the storage implementation to use */ public constructor( context: Context, - apiClient: AuthenticationAPIClient, - storage: Storage + auth0: Auth0, + storage: Storage, ) : this( - apiClient, + AuthenticationAPIClient(auth0), storage, CryptoUtil(context, storage, KEY_ALIAS), JWTDecoder(), - Executors.newSingleThreadExecutor() + auth0.executor ) - /** - * 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 { - 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 - } - } - 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. + * Creates a new SecureCredentialsManager to handle Credentials with biometrics Authentication * - * @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. + * @param context a valid context + * @param auth0 the Auth0 account information to use + * @param storage the storage implementation to use + * @param fragmentActivity the FragmentActivity to use for the biometric authentication + * @param localAuthenticationOptions the options of type [LocalAuthenticationOptions] to use for the biometric authentication */ - public fun checkAuthenticationResult(requestCode: Int, resultCode: Int): Boolean { - if (requestCode != authenticationRequestCode || decryptCallback == null) { - 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 - } - return true - } + public constructor( + context: Context, + auth0: Auth0, + storage: Storage, + fragmentActivity: FragmentActivity, + localAuthenticationOptions: LocalAuthenticationOptions + ) : this( + AuthenticationAPIClient(auth0), + storage, + CryptoUtil(context, storage, KEY_ALIAS), + JWTDecoder(), + auth0.executor, + WeakReference(fragmentActivity), + localAuthenticationOptions, + DefaultLocalAuthenticationManagerFactory() + ) /** * Saves the given credentials in the Storage. @@ -162,10 +92,9 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT * implementation and will have [CredentialsManagerException.isDeviceIncompatible] return true. */ @Throws(CredentialsManagerException::class) - @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) @@ -181,10 +110,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) { /* @@ -194,7 +121,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT */ 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.", + CredentialsManagerException.Code.CRYPTO_EXCEPTION, e ) } @@ -206,13 +133,13 @@ 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. + * */ @JvmSynthetic @Throws(CredentialsManagerException::class) - public suspend fun awaitCredentials(): Credentials { + override suspend fun awaitCredentials(): Credentials { return awaitCredentials(null, 0) } @@ -222,16 +149,18 @@ 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 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 { + override suspend fun awaitCredentials( + scope: String?, + minTtl: Int + ): Credentials { return awaitCredentials(scope, minTtl, emptyMap()) } @@ -241,9 +170,8 @@ 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 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. @@ -251,12 +179,17 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT */ @JvmSynthetic @Throws(CredentialsManagerException::class) - public suspend fun awaitCredentials( + override suspend fun awaitCredentials( scope: String?, minTtl: Int, parameters: Map ): Credentials { - return awaitCredentials(scope, minTtl, parameters, false) + return awaitCredentials( + scope, + minTtl, + parameters, + false + ) } /** @@ -265,9 +198,8 @@ 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 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. @@ -276,13 +208,19 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT */ @JvmSynthetic @Throws(CredentialsManagerException::class) - public suspend fun awaitCredentials( + override suspend fun awaitCredentials( scope: String?, minTtl: Int, parameters: Map, forceRefresh: Boolean, ): Credentials { - return awaitCredentials(scope, minTtl, parameters, mapOf(), forceRefresh) + return awaitCredentials( + scope, + minTtl, + parameters, + mapOf(), + forceRefresh + ) } /** @@ -291,9 +229,8 @@ 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 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. @@ -303,7 +240,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT */ @JvmSynthetic @Throws(CredentialsManagerException::class) - public suspend fun awaitCredentials( + override suspend fun awaitCredentials( scope: String?, minTtl: Int, parameters: Map, @@ -334,14 +271,14 @@ 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 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. + * 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 callback the callback to receive the result in. */ - override fun getCredentials(callback: Callback) { + override fun getCredentials( + callback: Callback + ) { getCredentials(null, 0, callback) } @@ -350,10 +287,8 @@ 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 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 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. @@ -372,23 +307,27 @@ 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 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 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( + override fun getCredentials( scope: String?, minTtl: Int, parameters: Map, callback: Callback ) { - getCredentials(scope, minTtl, parameters, false, callback) + getCredentials( + scope, + minTtl, + parameters, + false, + callback + ) } /** @@ -397,24 +336,30 @@ 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 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( + override fun getCredentials( scope: String?, minTtl: Int, parameters: Map, forceRefresh: Boolean, callback: Callback ) { - getCredentials(scope, minTtl, parameters, mapOf(), forceRefresh, callback) + getCredentials( + scope, + minTtl, + parameters, + mapOf(), + forceRefresh, + callback + ) } /** @@ -423,18 +368,16 @@ 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 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( + override fun getCredentials( scope: String?, minTtl: Int, parameters: Map, @@ -443,25 +386,50 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT callback: Callback ) { if (!hasValidCredentials(minTtl.toLong())) { - callback.onFailure(CredentialsManagerException("No Credentials were previously set.")) + callback.onFailure(CredentialsManagerException.NO_CREDENTIALS) return } - if (authenticateBeforeDecrypt) { - Log.d( - TAG, - "Authentication is required to read the Credentials. Showing the LockScreen." - ) - decryptCallback = callback - this.scope = scope - this.minTtl = minTtl - this.forceRefresh = forceRefresh - activityResultContract?.launch(authIntent) - ?: activity?.startActivityForResult(authIntent, authenticationRequestCode) + + if (fragmentActivity != null && localAuthenticationOptions != null && localAuthenticationManagerFactory != null) { + fragmentActivity.get()?.let { fragmentActivity -> + val localAuthenticationManager = localAuthenticationManagerFactory.create( + activity = fragmentActivity, + authenticationOptions = localAuthenticationOptions, + resultCallback = localAuthenticationResultCallback( + scope, + minTtl, + parameters, + headers, + forceRefresh, + callback + ) + ) + localAuthenticationManager.authenticate() + } ?: run { + callback.onFailure(CredentialsManagerException.BIOMETRIC_ERROR_NO_ACTIVITY) + } return } + continueGetCredentials(scope, minTtl, parameters, headers, forceRefresh, callback) } + private val localAuthenticationResultCallback = + { scope: String?, minTtl: Int, parameters: Map, headers: Map, forceRefresh: Boolean, callback: Callback -> + object : Callback { + override fun onSuccess(result: Boolean) { + continueGetCredentials( + scope, minTtl, parameters, headers, forceRefresh, + callback + ) + } + + override fun onFailure(error: CredentialsManagerException) { + callback.onFailure(error) + } + } + } + /** * Delete the stored credentials */ @@ -504,7 +472,8 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT (canRefresh == null || !canRefresh)) } - private fun continueGetCredentials( + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun continueGetCredentials( scope: String?, minTtl: Int, parameters: Map, @@ -515,8 +484,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT serialExecutor.execute { val encryptedEncoded = storage.retrieveString(KEY_CREDENTIALS) if (encryptedEncoded.isNullOrBlank()) { - callback.onFailure(CredentialsManagerException("No Credentials were previously set.")) - decryptCallback = null + callback.onFailure(CredentialsManagerException.NO_CREDENTIALS) return@execute } val encrypted = Base64.decode(encryptedEncoded, Base64.DEFAULT) @@ -526,25 +494,20 @@ 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 ) ) - decryptCallback = null return@execute } catch (e: CryptoException) { //If keys were invalidated, existing credentials will not be recoverable. 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 ) ) - decryptCallback = null return@execute } val bridgeCredentials = gson.fromJson(json, OptionalCredentials::class.java) @@ -564,20 +527,17 @@ 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.")) - decryptCallback = null + callback.onFailure(CredentialsManagerException.NO_CREDENTIALS) 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) { - callback.onFailure(CredentialsManagerException("No Credentials were previously set.")) - decryptCallback = null + callback.onFailure(CredentialsManagerException.NO_REFRESH_TOKEN) return@execute } Log.d(TAG, "Credentials have expired. Renewing them now...") @@ -602,6 +562,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT 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.", @@ -610,7 +571,6 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT ) ) callback.onFailure(wrongTtlException) - decryptCallback = null return@execute } @@ -628,11 +588,10 @@ 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 ) ) - decryptCallback = null return@execute } @@ -641,28 +600,39 @@ 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) { + CredentialsManagerException.Code.STORE_FAILED, error + ) + if (error.cause is IncompatibleDeviceException || error.cause is CryptoException) { exception.refreshedCredentials = freshCredentials } callback.onFailure(exception) } - decryptCallback = null } } + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun clearFragmentActivity() { + fragmentActivity!!.clear() + } + internal companion object { 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" + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal const val KEY_CREDENTIALS = "com.auth0.credentials" + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal 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" - private const val KEY_CAN_REFRESH = "com.auth0.credentials_can_refresh" - private const val KEY_ALIAS = "com.auth0.key" - } + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal const val LEGACY_KEY_CACHE_EXPIRES_AT = "com.auth0.credentials_expires_at" + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal const val KEY_CAN_REFRESH = "com.auth0.credentials_can_refresh" - init { - authenticateBeforeDecrypt = false + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal const val KEY_ALIAS = "com.auth0.key" } } \ No newline at end of file diff --git a/auth0/src/main/java/com/auth0/android/management/UsersAPIClient.kt b/auth0/src/main/java/com/auth0/android/management/UsersAPIClient.kt index fca4fa6dd..43f23af44 100755 --- a/auth0/src/main/java/com/auth0/android/management/UsersAPIClient.kt +++ b/auth0/src/main/java/com/auth0/android/management/UsersAPIClient.kt @@ -3,6 +3,7 @@ package com.auth0.android.management import androidx.annotation.VisibleForTesting import com.auth0.android.Auth0 import com.auth0.android.Auth0Exception +import com.auth0.android.NetworkErrorException import com.auth0.android.authentication.ParameterBuilder import com.auth0.android.request.ErrorAdapter import com.auth0.android.request.JsonAdapter @@ -14,6 +15,7 @@ import com.auth0.android.request.internal.GsonAdapter.Companion.forListOf import com.auth0.android.request.internal.GsonAdapter.Companion.forMap import com.auth0.android.request.internal.GsonProvider import com.auth0.android.request.internal.RequestFactory +import com.auth0.android.request.internal.ResponseUtils.isNetworkError import com.auth0.android.result.UserIdentity import com.auth0.android.result.UserProfile import com.google.gson.Gson @@ -24,7 +26,7 @@ import java.io.Reader /** * API client for Auth0 Management API. * ``` - * val auth0 = Auth0("your_client_id", "your_domain") + * val auth0 = Auth0.getInstance("your_client_id", "your_domain") * val client = UsersAPIClient(auth0) * ``` * @@ -219,6 +221,12 @@ public class UsersAPIClient @VisibleForTesting(otherwise = VisibleForTesting.PRI } override fun fromException(cause: Throwable): ManagementException { + if (isNetworkError(cause)) { + return ManagementException( + "Failed to execute the network request", + NetworkErrorException(cause) + ) + } return ManagementException( "Something went wrong", Auth0Exception("Something went wrong", cause) diff --git a/auth0/src/main/java/com/auth0/android/request/DefaultClient.kt b/auth0/src/main/java/com/auth0/android/request/DefaultClient.kt index c0c676b6d..a00a57d8c 100644 --- a/auth0/src/main/java/com/auth0/android/request/DefaultClient.kt +++ b/auth0/src/main/java/com/auth0/android/request/DefaultClient.kt @@ -24,6 +24,7 @@ public class DefaultClient @VisibleForTesting(otherwise = VisibleForTesting.PRIV readTimeout: Int, private val defaultHeaders: Map, enableLogging: Boolean, + private val gson: Gson, sslSocketFactory: SSLSocketFactory?, trustManager: X509TrustManager? ) : NetworkingClient { @@ -40,11 +41,8 @@ public class DefaultClient @VisibleForTesting(otherwise = VisibleForTesting.PRIV connectTimeout: Int = DEFAULT_TIMEOUT_SECONDS, readTimeout: Int = DEFAULT_TIMEOUT_SECONDS, defaultHeaders: Map = mapOf(), - enableLogging: Boolean = false - ) : this(connectTimeout, readTimeout, defaultHeaders, enableLogging, null, null) - - //TODO: receive this via internal constructor parameters - private val gson: Gson = GsonProvider.gson + enableLogging: Boolean = false, + ) : this(connectTimeout, readTimeout, defaultHeaders, enableLogging, GsonProvider.gson, null, null) @get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal val okHttpClient: OkHttpClient @@ -71,6 +69,7 @@ public class DefaultClient @VisibleForTesting(otherwise = VisibleForTesting.PRIV .map { urlBuilder.addQueryParameter(it.key, it.value as String) } requestBuilder.method(options.method.toString(), null) } + else -> { // add parameters as body val body = gson.toJson(options.parameters).toRequestBody(APPLICATION_JSON_UTF8) @@ -90,7 +89,6 @@ public class DefaultClient @VisibleForTesting(otherwise = VisibleForTesting.PRIV val builder = OkHttpClient.Builder() // logging - //TODO: OFF by default! if (enableLogging) { val logger: Interceptor = HttpLoggingInterceptor() .setLevel(HttpLoggingInterceptor.Level.BODY) diff --git a/auth0/src/main/java/com/auth0/android/request/Request.kt b/auth0/src/main/java/com/auth0/android/request/Request.kt index 7bba4a29a..e6825e9f5 100755 --- a/auth0/src/main/java/com/auth0/android/request/Request.kt +++ b/auth0/src/main/java/com/auth0/android/request/Request.kt @@ -21,10 +21,6 @@ public interface Request { * Performs an async HTTP request against Auth0 API inside a Coroutine * This is a Coroutine that is exposed only for Kotlin. * - * Note: This method was added after the interface was released. - * It is defined as a default method for compatibility reasons. - * From version 3.0 on, the method will be abstract and all implementations of this interface - * will have to provide their own implementation. * * The default implementation throws an [UnsupportedOperationException]. * @@ -32,9 +28,7 @@ public interface Request { */ @JvmSynthetic @Throws(Auth0Exception::class) - public suspend fun await(): T { - throw UnsupportedOperationException("await") - } + public suspend fun await(): T /** * Executes the HTTP request against Auth0 API (blocking the current thread) diff --git a/auth0/src/main/java/com/auth0/android/request/internal/ResponseUtils.java b/auth0/src/main/java/com/auth0/android/request/internal/ResponseUtils.java deleted file mode 100644 index 6b9ae5e1a..000000000 --- a/auth0/src/main/java/com/auth0/android/request/internal/ResponseUtils.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.auth0.android.request.internal; - -import java.io.Closeable; -import java.io.IOException; - -class ResponseUtils { - - /** - * Attempts to close a stream. No exception will be thrown if an IOException was raised. - * - * @param closeable the stream to close - */ - static void closeStream(Closeable closeable) { - try { - closeable.close(); - } catch (IOException ignored) { - } - } -} diff --git a/auth0/src/main/java/com/auth0/android/request/internal/ResponseUtils.kt b/auth0/src/main/java/com/auth0/android/request/internal/ResponseUtils.kt new file mode 100644 index 000000000..fc80234b1 --- /dev/null +++ b/auth0/src/main/java/com/auth0/android/request/internal/ResponseUtils.kt @@ -0,0 +1,29 @@ +package com.auth0.android.request.internal + +import java.io.Closeable +import java.io.IOException +import java.net.SocketException +import java.net.SocketTimeoutException +import java.net.UnknownHostException + +internal object ResponseUtils { + /** + * Attempts to close a stream. No exception will be thrown if an IOException was raised. + * + * @param closeable the stream to close + */ + fun closeStream(closeable: Closeable) { + try { + closeable.close() + } catch (ignored: IOException) { + } + } + + /** + * Checks if the given Throwable is a network error. + */ + fun isNetworkError(cause: Throwable?): Boolean { + return (cause is SocketException || cause is SocketTimeoutException + || cause is UnknownHostException) + } +} diff --git a/auth0/src/main/java/com/auth0/android/result/Credentials.kt b/auth0/src/main/java/com/auth0/android/result/Credentials.kt index 841665633..42904212f 100755 --- a/auth0/src/main/java/com/auth0/android/result/Credentials.kt +++ b/auth0/src/main/java/com/auth0/android/result/Credentials.kt @@ -1,6 +1,5 @@ package com.auth0.android.result -import androidx.annotation.VisibleForTesting import com.auth0.android.request.internal.GsonProvider import com.auth0.android.request.internal.Jwt import com.google.gson.annotations.SerializedName @@ -18,7 +17,7 @@ import java.util.* * * *scope*: The token's granted scope. * */ -public open class Credentials( +public data class Credentials( /** * Getter for the Identity Token with user information. * @@ -61,11 +60,6 @@ public open class Credentials( @field:SerializedName("scope") public val scope: String? ) { - //TODO this could be removed and the class be a data class instead - @get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal open val currentTimeInMillis: Long - get() = System.currentTimeMillis() - /** * Getter for the new multi-factor authentication recovery code. Only available if these credentials are the result of logging in using an MFA recovery code. * @@ -79,10 +73,11 @@ public open class Credentials( return "Credentials(idToken='xxxxx', accessToken='xxxxx', type='$type', refreshToken='xxxxx', expiresAt='$expiresAt', scope='$scope')" } - public val user: UserProfile get() { - val (_, payload) = Jwt.splitToken(idToken) - val gson = GsonProvider.gson - return gson.fromJson(Jwt.decodeBase64(payload), UserProfile::class.java) - } + public val user: UserProfile + get() { + val (_, payload) = Jwt.splitToken(idToken) + val gson = GsonProvider.gson + return gson.fromJson(Jwt.decodeBase64(payload), UserProfile::class.java) + } } \ No newline at end of file diff --git a/auth0/src/test/java/com/auth0/android/Auth0Test.java b/auth0/src/test/java/com/auth0/android/Auth0Test.java index e76e18a26..f924a95bb 100755 --- a/auth0/src/test/java/com/auth0/android/Auth0Test.java +++ b/auth0/src/test/java/com/auth0/android/Auth0Test.java @@ -61,7 +61,7 @@ public void shouldBuildFromResources() { when(context.getString(eq(222))).thenReturn(CLIENT_ID); when(context.getString(eq(333))).thenReturn(DOMAIN); - Auth0 auth0 = new Auth0(context); + Auth0 auth0 = Auth0.getInstance(context); assertThat(auth0, notNullValue()); assertThat(auth0.getClientId(), equalTo(CLIENT_ID)); @@ -77,7 +77,7 @@ public void shouldFailToBuildFromResourcesWithoutClientID() { when(resources.getIdentifier(eq("com_auth0_client_id"), eq("string"), eq(PACKAGE_NAME))).thenReturn(0); when(resources.getIdentifier(eq("com_auth0_domain"), eq("string"), eq(PACKAGE_NAME))).thenReturn(333); - new Auth0(context); + Auth0.getInstance(context); }); } @@ -89,13 +89,13 @@ public void shouldFailToBuildFromResourcesWithoutDomain() { when(resources.getIdentifier(eq("com_auth0_client_id"), eq("string"), eq(PACKAGE_NAME))).thenReturn(222); when(resources.getIdentifier(eq("com_auth0_domain"), eq("string"), eq(PACKAGE_NAME))).thenReturn(0); - new Auth0(context); + Auth0.getInstance(context); }); } @Test public void shouldBuildWithClientIdAndDomain() { - Auth0 auth0 = new Auth0(CLIENT_ID, DOMAIN); + Auth0 auth0 = Auth0.getInstance(CLIENT_ID, DOMAIN, null); assertThat(auth0.getClientId(), equalTo(CLIENT_ID)); assertThat(HttpUrl.parse(auth0.getDomainUrl()), equalTo(HttpUrl.parse("https://samples.auth0.com"))); assertThat(HttpUrl.parse(auth0.getConfigurationUrl()), equalTo(HttpUrl.parse("https://samples.auth0.com"))); @@ -103,7 +103,7 @@ public void shouldBuildWithClientIdAndDomain() { @Test public void shouldBuildWithConfigurationDomainToo() { - Auth0 auth0 = new Auth0(CLIENT_ID, DOMAIN, CONFIG_DOMAIN_CUSTOM); + Auth0 auth0 = Auth0.getInstance(CLIENT_ID, DOMAIN, CONFIG_DOMAIN_CUSTOM); assertThat(auth0.getClientId(), equalTo(CLIENT_ID)); assertThat(HttpUrl.parse(auth0.getDomainUrl()), equalTo(HttpUrl.parse("https://samples.auth0.com"))); assertThat(HttpUrl.parse(auth0.getConfigurationUrl()), equalTo(HttpUrl.parse("https://config.mydomain.com"))); @@ -111,7 +111,7 @@ public void shouldBuildWithConfigurationDomainToo() { @Test public void shouldHandleEUInstance() { - Auth0 auth0 = new Auth0(CLIENT_ID, EU_DOMAIN); + Auth0 auth0 = Auth0.getInstance(CLIENT_ID, EU_DOMAIN); assertThat(auth0.getClientId(), equalTo(CLIENT_ID)); assertThat(HttpUrl.parse(auth0.getDomainUrl()), equalTo(HttpUrl.parse("https://samples.eu.auth0.com"))); assertThat(HttpUrl.parse(auth0.getConfigurationUrl()), equalTo(HttpUrl.parse("https://samples.eu.auth0.com"))); @@ -119,7 +119,7 @@ public void shouldHandleEUInstance() { @Test public void shouldHandleAUInstance() { - Auth0 auth0 = new Auth0(CLIENT_ID, AU_DOMAIN); + Auth0 auth0 = Auth0.getInstance(CLIENT_ID, AU_DOMAIN); assertThat(auth0.getClientId(), equalTo(CLIENT_ID)); assertThat(HttpUrl.parse(auth0.getDomainUrl()), equalTo(HttpUrl.parse("https://samples.au.auth0.com"))); assertThat(HttpUrl.parse(auth0.getConfigurationUrl()), equalTo(HttpUrl.parse("https://samples.au.auth0.com"))); @@ -127,7 +127,7 @@ public void shouldHandleAUInstance() { @Test public void shouldHandleOtherInstance() { - Auth0 auth0 = new Auth0(CLIENT_ID, OTHER_DOMAIN); + Auth0 auth0 = Auth0.getInstance(CLIENT_ID, OTHER_DOMAIN); assertThat(auth0.getClientId(), equalTo(CLIENT_ID)); assertThat(HttpUrl.parse(auth0.getDomainUrl()), equalTo(HttpUrl.parse("https://samples-test.other-subdomain.other.auth0.com"))); assertThat(HttpUrl.parse(auth0.getConfigurationUrl()), equalTo(HttpUrl.parse("https://samples-test.other-subdomain.other.auth0.com"))); @@ -135,7 +135,7 @@ public void shouldHandleOtherInstance() { @Test public void shouldHandleNonAuth0Domain() { - Auth0 auth0 = new Auth0(CLIENT_ID, "mydomain.com"); + Auth0 auth0 = Auth0.getInstance(CLIENT_ID, "mydomain.com"); assertThat(auth0.getClientId(), equalTo(CLIENT_ID)); assertThat(HttpUrl.parse(auth0.getDomainUrl()), equalTo(HttpUrl.parse("https://mydomain.com"))); assertThat(HttpUrl.parse(auth0.getConfigurationUrl()), equalTo(HttpUrl.parse("https://mydomain.com"))); @@ -143,12 +143,12 @@ public void shouldHandleNonAuth0Domain() { @Test public void shouldThrowWhenInvalidDomain() { - Assert.assertThrows(IllegalArgumentException.class, () -> new Auth0(CLIENT_ID, "some invalid domain.com")); + Assert.assertThrows(IllegalArgumentException.class, () -> Auth0.getInstance(CLIENT_ID, "some invalid domain.com")); } @Test public void shouldReturnAuthorizeUrl() { - Auth0 auth0 = new Auth0(CLIENT_ID, DOMAIN); + Auth0 auth0 = Auth0.getInstance(CLIENT_ID, DOMAIN); final HttpUrl url = HttpUrl.parse(auth0.getAuthorizeUrl()); assertThat(url, hasScheme("https")); @@ -158,7 +158,7 @@ public void shouldReturnAuthorizeUrl() { @Test public void shouldReturnLogoutUrl() { - Auth0 auth0 = new Auth0(CLIENT_ID, DOMAIN); + Auth0 auth0 = Auth0.getInstance(CLIENT_ID, DOMAIN); final HttpUrl url = HttpUrl.parse(auth0.getLogoutUrl()); assertThat(url, hasScheme("https")); @@ -169,30 +169,30 @@ public void shouldReturnLogoutUrl() { @Test public void shouldSetCustomTelemetry() { Auth0UserAgent customAuth0UserAgent = new Auth0UserAgent("custom", "9.9.9", "1.1.1"); - Auth0 auth0 = new Auth0(CLIENT_ID, DOMAIN); + Auth0 auth0 = Auth0.getInstance(CLIENT_ID, DOMAIN); auth0.setAuth0UserAgent(customAuth0UserAgent); assertThat(auth0.getAuth0UserAgent(), is(equalTo(customAuth0UserAgent))); } @Test public void shouldThrowWhenHttpDomainUsed() { - Assert.assertThrows("Invalid domain url: 'http://" + DOMAIN + "'. Only HTTPS domain URLs are supported. If no scheme is passed, HTTPS will be used.", IllegalArgumentException.class, () -> new Auth0(CLIENT_ID, "http://" + DOMAIN)); + Assert.assertThrows("Invalid domain url: 'http://" + DOMAIN + "'. Only HTTPS domain URLs are supported. If no scheme is passed, HTTPS will be used.", IllegalArgumentException.class, () -> Auth0.getInstance(CLIENT_ID, "http://" + DOMAIN)); } @Test public void shouldHandleUpperCaseHttpsDomain() { - Auth0 auth0 = new Auth0(CLIENT_ID, "Https://" + DOMAIN); + Auth0 auth0 = Auth0.getInstance(CLIENT_ID, "Https://" + DOMAIN); assertThat(auth0.getDomainUrl(), is("https://" + DOMAIN + "/")); } @Test public void shouldThrowWhenHttpUppercaseDomainUsed() { - Assert.assertThrows("Invalid domain url: 'http://" + DOMAIN + "'. Only HTTPS domain URLs are supported. If no scheme is passed, HTTPS will be used.", IllegalArgumentException.class, () -> new Auth0(CLIENT_ID, "HTTP://" + DOMAIN)); + Assert.assertThrows("Invalid domain url: 'http://" + DOMAIN + "'. Only HTTPS domain URLs are supported. If no scheme is passed, HTTPS will be used.", IllegalArgumentException.class, () -> Auth0.getInstance(CLIENT_ID, "HTTP://" + DOMAIN)); } @Test public void shouldThrowWhenConfigDomainIsHttp() { - Assert.assertThrows("Invalid domain url: 'http://" + OTHER_DOMAIN + "'. Only HTTPS domain URLs are supported. If no scheme is passed, HTTPS will be used.", IllegalArgumentException.class, () -> new Auth0(CLIENT_ID, DOMAIN, "HTTP://" + OTHER_DOMAIN)); + Assert.assertThrows("Invalid domain url: 'http://" + OTHER_DOMAIN + "'. Only HTTPS domain URLs are supported. If no scheme is passed, HTTPS will be used.", IllegalArgumentException.class, () -> Auth0.getInstance(CLIENT_ID, DOMAIN, "HTTP://" + OTHER_DOMAIN)); } @Test @@ -202,24 +202,24 @@ public void shouldEnsureAuthorizeUrlIsOpen() throws NoSuchMethodException { Assert.assertFalse(Modifier.isFinal(method.getModifiers())); } - @Test - public void shouldAllowOverridingAuthorizeUrl() { - class MyAuth extends Auth0 { - public MyAuth() { - super(CLIENT_ID, DOMAIN); - } - @Override - public String getAuthorizeUrl() { - // Calling super to make sure it's fully visible as well as overridable. - return super.getAuthorizeUrl() + "something"; - } - } - } - @Test public void shouldEnsureLogoutUrlIsOpen() throws NoSuchMethodException { Method method = Auth0.class.getMethod("getLogoutUrl"); Assert.assertTrue(Modifier.isPublic(method.getModifiers())); Assert.assertFalse(Modifier.isFinal(method.getModifiers())); } + + @Test + public void sameConfigShouldReturnSameInstance() { + Auth0 auth0 = Auth0.getInstance(CLIENT_ID, DOMAIN); + Auth0 auth0_2 = Auth0.getInstance(CLIENT_ID, DOMAIN); + Assert.assertSame(auth0, auth0_2); + } + + @Test + public void differentConfigShouldReturnDifferentInstances() { + Auth0 auth0 = Auth0.getInstance(CLIENT_ID, DOMAIN); + Auth0 auth0_2 = Auth0.getInstance(CLIENT_ID + "2", DOMAIN + "2"); + Assert.assertNotSame(auth0, auth0_2); + } } \ No newline at end of file diff --git a/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt b/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt index a2c3af434..8212c2184 100755 --- a/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt @@ -63,7 +63,7 @@ public class AuthenticationAPIClientTest { @Test public fun shouldUseCustomNetworkingClient() { - val account = Auth0("client-id", "https://tenant.auth0.com/") + val account = Auth0.getInstance("client-id", "https://tenant.auth0.com/") val jsonResponse = FileReader("src/test/resources/credentials_openid.json").readText() val inputStream: InputStream = ByteArrayInputStream(jsonResponse.toByteArray()) val response = ServerResponse(200, inputStream, emptyMap()) @@ -94,7 +94,7 @@ public class AuthenticationAPIClientTest { val auth0UserAgent: Auth0UserAgent = mock() whenever(auth0UserAgent.value).thenReturn("the-user-agent-data") val factory: RequestFactory = mock() - val account = Auth0(CLIENT_ID, DOMAIN) + val account = Auth0.getInstance(CLIENT_ID, DOMAIN) account.auth0UserAgent = auth0UserAgent AuthenticationAPIClient(account, factory, gson) verify(factory).setAuth0ClientInfo("the-user-agent-data") @@ -102,7 +102,7 @@ public class AuthenticationAPIClientTest { @Test public fun shouldCreateClientWithAccountInfo() { - val client = AuthenticationAPIClient(Auth0(CLIENT_ID, DOMAIN)) + val client = AuthenticationAPIClient(Auth0.getInstance(CLIENT_ID, DOMAIN)) assertThat(client, Matchers.`is`(Matchers.notNullValue())) assertThat(client.clientId, Matchers.equalTo(CLIENT_ID)) assertThat(client.baseURL.toHttpUrlOrNull()!!, Matchers.notNullValue()) @@ -134,7 +134,7 @@ public class AuthenticationAPIClientTest { ).thenReturn(333) whenever(context.getString(eq(222))).thenReturn(CLIENT_ID) whenever(context.getString(eq(333))).thenReturn(DOMAIN) - val client = AuthenticationAPIClient(Auth0(context)) + val client = AuthenticationAPIClient(Auth0.getInstance(context)) assertThat(client, Matchers.`is`(Matchers.notNullValue())) assertThat(client.clientId, Matchers.`is`(CLIENT_ID)) assertThat(client.baseURL, Matchers.equalTo("https://" + DOMAIN + "/")) @@ -2411,7 +2411,7 @@ public class AuthenticationAPIClientTest { } private val auth0: Auth0 get() { - val auth0 = Auth0(CLIENT_ID, mockAPI.domain, mockAPI.domain) + val auth0 = Auth0.getInstance(CLIENT_ID, mockAPI.domain, mockAPI.domain) auth0.networkingClient = testClient return auth0 } diff --git a/auth0/src/test/java/com/auth0/android/authentication/AuthenticationExceptionTest.kt b/auth0/src/test/java/com/auth0/android/authentication/AuthenticationExceptionTest.kt index 97c37343b..e1bb2cfb9 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/AuthenticationExceptionTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/AuthenticationExceptionTest.kt @@ -205,7 +205,7 @@ public class AuthenticationExceptionTest { @Test public fun shouldHaveNetworkErrorForSocketTimeout() { val ex = AuthenticationException( - "Request has definitely failed", Auth0Exception("", + "Request has definitely failed", NetworkErrorException( SocketTimeoutException() ) ) 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 19cd0cbf7..98c4faff6 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 @@ -73,7 +73,7 @@ public class CredentialsManagerTest { val refreshToken = invocation.getArgument(3, String::class.java) val expiresAt = invocation.getArgument(4, Date::class.java) val scope = invocation.getArgument(5, String::class.java) - CredentialsMock(idToken, accessToken, type, refreshToken, expiresAt, scope) + CredentialsMock.create(idToken, accessToken, type, refreshToken, expiresAt, scope) }.`when`(manager).recreateCredentials( ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), @@ -87,7 +87,7 @@ public class CredentialsManagerTest { @Test public fun shouldSaveRefreshableCredentialsInStorage() { val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS - val credentials: Credentials = CredentialsMock( + val credentials: Credentials = CredentialsMock.create( "idToken", "accessToken", "type", @@ -110,7 +110,7 @@ public class CredentialsManagerTest { @Test public fun shouldSaveRefreshableCredentialsUsingAccessTokenExpForCacheExpirationInStorage() { val accessTokenExpirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS - val credentials: Credentials = CredentialsMock( + val credentials: Credentials = CredentialsMock.create( "", "accessToken", "type", @@ -134,7 +134,7 @@ public class CredentialsManagerTest { public fun shouldSaveRefreshableCredentialsIgnoringIdTokenExpForCacheExpirationInStorage() { val accessTokenExpirationTime = CredentialsMock.CURRENT_TIME_MS + 5000 * 1000 val idTokenExpirationTime = CredentialsMock.CURRENT_TIME_MS + 2000 * 1000 - val credentials: Credentials = CredentialsMock( + val credentials: Credentials = CredentialsMock.create( "idToken", "accessToken", "type", @@ -158,7 +158,14 @@ public class CredentialsManagerTest { public fun shouldSaveNonRefreshableCredentialsInStorage() { val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS val credentials: Credentials = - CredentialsMock("idToken", "accessToken", "type", null, Date(expirationTime), "scope") + CredentialsMock.create( + "idToken", + "accessToken", + "type", + null, + Date(expirationTime), + "scope" + ) prepareJwtDecoderMock(Date(expirationTime)) manager.saveCredentials(credentials) verify(storage).store("com.auth0.id_token", "idToken") @@ -174,23 +181,23 @@ 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) + CredentialsMock.create("", "", "type", "refreshToken", Date(), null) manager.saveCredentials(credentials) } @Test public fun shouldNotThrowOnSetIfCredentialsHasAccessTokenAndExpiresAt() { val credentials: Credentials = - CredentialsMock("", "accessToken", "type", "refreshToken", Date(), null) + CredentialsMock.create("", "accessToken", "type", "refreshToken", Date(), null) manager.saveCredentials(credentials) } @Test public fun shouldNotThrowOnSetIfCredentialsHasIdTokenAndExpiresAt() { val credentials: Credentials = - CredentialsMock("idToken", "", "type", "refreshToken", Date(), null) + CredentialsMock.create("idToken", "", "type", "refreshToken", Date(), null) prepareJwtDecoderMock(Date()) manager.saveCredentials(credentials) } @@ -946,8 +953,8 @@ public class CredentialsManagerTest { throw IllegalArgumentException("Proper Executor Set") } manager.getCredentials(object : Callback { - override fun onSuccess(result: Credentials) { } - override fun onFailure(error: CredentialsManagerException) { } + override fun onSuccess(result: Credentials) {} + override fun onFailure(error: CredentialsManagerException) {} }) } @@ -1024,12 +1031,21 @@ public class CredentialsManagerTest { 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)) } @@ -1045,7 +1061,8 @@ public class CredentialsManagerTest { Mockito.`when`(storage.retrieveLong("com.auth0.cache_expires_at")) .thenReturn(expirationTime) Mockito.`when`(storage.retrieveString("com.auth0.scope")).thenReturn("scope") - manager.getCredentials("scope", + manager.getCredentials( + "scope", 0, emptyMap(), false, 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 000000000..53c14baf1 --- /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 { + setTitle(title) + setSubTitle(subtitle) + setDescription(description) + setNegativeButtonText(negativeButtonText) + setAuthenticationLevel(authenticator) + setDeviceCredentialFallback(enableDeviceCredentialFallback) + } + return builder.build() + } +} + 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 17d6075aa..6a78c50c7 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 @@ -24,9 +16,14 @@ import com.auth0.android.result.Credentials import com.auth0.android.result.CredentialsMock import com.auth0.android.util.Clock import com.google.gson.Gson -import com.nhaarman.mockitokotlin2.* +import com.nhaarman.mockitokotlin2.KArgumentCaptor import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.argumentCaptor import com.nhaarman.mockitokotlin2.eq +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.never +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest @@ -35,29 +32,33 @@ import org.hamcrest.Matchers import org.hamcrest.core.Is import org.hamcrest.core.IsInstanceOf import org.junit.Assert +import org.junit.Assert.assertThrows import org.junit.Before 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.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyLong +import org.mockito.ArgumentMatchers.anyString 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.lang.ref.WeakReference +import java.util.Date +import java.util.concurrent.CountDownLatch import java.util.concurrent.Executor -import org.junit.Assert.assertThrows -import org.junit.Assert.assertTrue -import java.lang.Exception +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + @RunWith(RobolectricTestRunner::class) public class SecureCredentialsManagerTest { + @Mock private lateinit var client: AuthenticationAPIClient @@ -76,6 +77,16 @@ public class SecureCredentialsManagerTest { @Mock private lateinit var jwtDecoder: JWTDecoder + @Mock + private lateinit var factory: LocalAuthenticationManagerFactory + + @Mock + private lateinit var localAuthenticationManager: LocalAuthenticationManager + + private lateinit var weakFragmentActivity: WeakReference + + private lateinit var fragmentActivity: FragmentActivity + private val serialExecutor = Executor { runnable -> runnable.run() } private val credentialsCaptor: KArgumentCaptor = argumentCaptor() @@ -88,6 +99,7 @@ public class SecureCredentialsManagerTest { public val exception: ExpectedException = ExpectedException.none() private lateinit var manager: SecureCredentialsManager private lateinit var gson: Gson + private lateinit var auth0: Auth0 @Before public fun setUp() { @@ -98,8 +110,32 @@ 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() + ) + weakFragmentActivity = WeakReference(fragmentActivity) + auth0 = Mockito.spy(Auth0.getInstance("clientId", "domain")) + Mockito.`when`(auth0.executor).thenReturn(serialExecutor) + val secureCredentialsManager = - SecureCredentialsManager(client, storage, crypto, jwtDecoder, serialExecutor) + SecureCredentialsManager( + client, + storage, + crypto, + jwtDecoder, + auth0.executor, + weakFragmentActivity, + getAuthenticationOptions(), + factory + ) manager = Mockito.spy(secureCredentialsManager) Mockito.doReturn(CredentialsMock.CURRENT_TIME_MS).`when`(manager).currentTimeInMillis gson = GsonProvider.gson @@ -109,9 +145,14 @@ public class SecureCredentialsManagerTest { public fun shouldCreateAManagerInstance() { val context: Context = Robolectric.buildActivity(Activity::class.java).create().start().resume().get() - val apiClient = AuthenticationAPIClient(Auth0("clientId", "domain")) val storage: Storage = SharedPreferencesStorage(context) - val manager = SecureCredentialsManager(context, apiClient, storage) + val manager = SecureCredentialsManager( + context, + auth0, + storage, + fragmentActivity, + getAuthenticationOptions() + ) MatcherAssert.assertThat(manager, Is.`is`(Matchers.notNullValue())) } @@ -121,7 +162,7 @@ public class SecureCredentialsManagerTest { @Test public fun shouldSaveRefreshableCredentialsInStorage() { val sharedExpirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS - val credentials: Credentials = CredentialsMock( + val credentials: Credentials = CredentialsMock.create( "idToken", "accessToken", "type", @@ -156,7 +197,7 @@ public class SecureCredentialsManagerTest { @Test public fun shouldSaveRefreshableCredentialsUsingAccessTokenExpForCacheExpirationInStorage() { val accessTokenExpirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS - val credentials: Credentials = CredentialsMock( + val credentials: Credentials = CredentialsMock.create( "", "accessToken", "type", @@ -195,7 +236,7 @@ public class SecureCredentialsManagerTest { public fun shouldSaveRefreshableCredentialsIgnoringIdTokenExpForCacheExpirationInStorage() { val accessTokenExpirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS val idTokenExpirationTime = CredentialsMock.CURRENT_TIME_MS + 2000 * 1000 - val credentials: Credentials = CredentialsMock( + val credentials: Credentials = CredentialsMock.create( "idToken", "accessToken", "type", @@ -234,7 +275,14 @@ public class SecureCredentialsManagerTest { public fun shouldSaveNonRefreshableCredentialsInStorage() { val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS val credentials: Credentials = - CredentialsMock("idToken", "accessToken", "type", null, Date(expirationTime), "scope") + CredentialsMock.create( + "idToken", + "accessToken", + "type", + null, + Date(expirationTime), + "scope" + ) val json = gson.toJson(credentials) prepareJwtDecoderMock(Date(expirationTime)) Mockito.`when`(crypto.encrypt(json.toByteArray())).thenReturn(json.toByteArray()) @@ -262,7 +310,7 @@ public class SecureCredentialsManagerTest { @Test public fun shouldClearStoredCredentialsAndThrowOnSaveOnCryptoException() { val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS - val credentials: Credentials = CredentialsMock( + val credentials: Credentials = CredentialsMock.create( "idToken", "accessToken", "type", @@ -284,7 +332,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") @@ -294,7 +342,7 @@ public class SecureCredentialsManagerTest { @Test public fun shouldThrowOnSaveOnIncompatibleDeviceException() { val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS - val credentials: Credentials = CredentialsMock( + val credentials: Credentials = CredentialsMock.create( "idToken", "accessToken", "type", @@ -322,16 +370,16 @@ 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") + CredentialsMock.create("", "", "type", "refreshToken", Date(), "scope") manager.saveCredentials(credentials) } @Test public fun shouldNotThrowOnSaveIfCredentialsHaveAccessTokenAndExpiresIn() { val credentials: Credentials = - CredentialsMock("", "accessToken", "type", "refreshToken", Date(), "scope") + CredentialsMock.create("", "accessToken", "type", "refreshToken", Date(), "scope") Mockito.`when`(crypto.encrypt(any())) .thenReturn(byteArrayOf(12, 34, 56, 78)) manager.saveCredentials(credentials) @@ -340,7 +388,7 @@ public class SecureCredentialsManagerTest { @Test public fun shouldNotThrowOnSaveIfCredentialsHaveIdTokenAndExpiresIn() { val credentials: Credentials = - CredentialsMock("idToken", "", "type", "refreshToken", Date(), "scope") + CredentialsMock.create("idToken", "", "type", "refreshToken", Date(), "scope") prepareJwtDecoderMock(Date()) Mockito.`when`(crypto.encrypt(any())) .thenReturn(byteArrayOf(12, 34, 56, 78)) @@ -352,6 +400,9 @@ public class SecureCredentialsManagerTest { */ @Test public fun shouldClearStoredCredentialsAndFailOnGetCredentialsWhenCryptoExceptionIsThrown() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } verifyNoMoreInteractions(client) val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) val storedJson = insertTestCredentials( @@ -387,6 +438,9 @@ public class SecureCredentialsManagerTest { @Test public fun shouldFailOnGetCredentialsWhenIncompatibleDeviceExceptionIsThrown() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } verifyNoMoreInteractions(client) val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) val storedJson = insertTestCredentials( @@ -420,6 +474,9 @@ public class SecureCredentialsManagerTest { @Test public fun shouldFailOnSavingRefreshedCredentialsInGetCredentialsWhenCryptoExceptionIsThrown() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) // non expired credentials insertTestCredentials(false, true, true, expiresAt, "scope") // "scope" is set val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS + ONE_HOUR_SECONDS * 1000) @@ -438,7 +495,11 @@ 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, + callback + ) // minTTL of 0 seconds (default) verify(request) .addParameter(eq("scope"), eq("different scope")) verify(callback).onFailure( @@ -449,8 +510,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")) @@ -462,6 +529,9 @@ public class SecureCredentialsManagerTest { @Test public fun shouldFailOnSavingRefreshedCredentialsInGetCredentialsWhenIncompatibleDeviceExceptionIsThrown() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) // non expired credentials insertTestCredentials(false, true, true, expiresAt, "scope") // "scope" is set val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS + ONE_HOUR_SECONDS * 1000) @@ -480,7 +550,11 @@ 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, + callback + ) // minTTL of 0 seconds (default) verify(request) .addParameter(eq("scope"), eq("different scope")) verify(callback).onFailure( @@ -491,8 +565,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")) @@ -505,6 +585,9 @@ public class SecureCredentialsManagerTest { @Test public fun shouldFailWithoutRefreshedCredentialsInExceptionOnSavingRefreshedCredentialsInGetCredentialsWhenDifferentExceptionIsThrown() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) // non expired credentials insertTestCredentials(false, true, true, expiresAt, "scope") // "scope" is set val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS + ONE_HOUR_SECONDS * 1000) @@ -520,7 +603,11 @@ 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, + callback + ) // minTTL of 0 seconds (default) verify(request) .addParameter(eq("scope"), eq("different scope")) verify(callback).onFailure( @@ -531,13 +618,22 @@ 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())) } @Test public fun shouldFailOnGetCredentialsWhenNoAccessTokenOrIdTokenWasSaved() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } verifyNoMoreInteractions(client) val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) insertTestCredentials(false, false, true, expiresAt, "scope") @@ -552,6 +648,9 @@ public class SecureCredentialsManagerTest { @Test public fun shouldFailOnGetCredentialsWhenExpiredAndNoRefreshTokenWasSaved() { + 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") @@ -561,11 +660,17 @@ public class SecureCredentialsManagerTest { ) 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 public fun shouldGetNonExpiredCredentialsFromStorage() { + 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") @@ -587,23 +692,29 @@ 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() + 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") @@ -611,11 +722,17 @@ public class SecureCredentialsManagerTest { runBlocking { manager.awaitCredentials() } } 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 public fun shouldGetNonExpiredCredentialsFromStorageWhenOnlyIdTokenIsAvailable() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } verifyNoMoreInteractions(client) val expiresAt = Date(CredentialsMock.CURRENT_TIME_MS + ONE_HOUR_SECONDS * 1000) insertTestCredentials(true, false, true, expiresAt, "scope") @@ -636,6 +753,9 @@ public class SecureCredentialsManagerTest { @Test public fun shouldGetNonExpiredCredentialsFromStorageWhenOnlyAccessTokenIsAvailable() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } verifyNoMoreInteractions(client) val expiresAt = Date(CredentialsMock.CURRENT_TIME_MS + ONE_HOUR_SECONDS * 1000) insertTestCredentials(false, true, true, expiresAt, "scope") @@ -656,6 +776,9 @@ public class SecureCredentialsManagerTest { @Test public fun shouldRenewCredentialsWithMinTtl() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } val expiresAt = Date(CredentialsMock.CURRENT_TIME_MS) // expired credentials insertTestCredentials(false, true, true, expiresAt, "scope") val newDate = @@ -672,7 +795,14 @@ 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.continueGetCredentials( + null, + 60, + emptyMap(), + emptyMap(), + false, + callback + ) // minTTL of 1 minute verify(request, never()) .addParameter(eq("scope"), anyString()) verify(callback).onSuccess( @@ -716,6 +846,9 @@ public class SecureCredentialsManagerTest { @Test public fun shouldGetAndFailToRenewExpiredCredentialsWhenReceivedTokenHasLowerTtl() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } val expiresAt = Date(CredentialsMock.CURRENT_TIME_MS) // expired credentials insertTestCredentials(false, true, true, expiresAt, "scope") val newDate = @@ -734,7 +867,14 @@ 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.continueGetCredentials( + null, + 60, + emptyMap(), + emptyMap(), + false, + callback + ) // minTTL of 1 minute verify(request, never()) .addParameter(eq("scope"), anyString()) verify(callback).onFailure( @@ -761,6 +901,9 @@ public class SecureCredentialsManagerTest { @Test public fun shouldRenewCredentialsWhenScopeHasChanged() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) // non expired credentials insertTestCredentials(false, true, true, expiresAt, "scope") // "scope" is set val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS + ONE_HOUR_SECONDS * 1000) @@ -779,7 +922,11 @@ 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, + callback + ) // minTTL of 0 seconds (default) verify(request) .addParameter(eq("scope"), eq("different scope")) verify(callback).onSuccess( @@ -823,6 +970,9 @@ public class SecureCredentialsManagerTest { @Test public fun shouldRenewCredentialsIfSavedScopeIsNullAndRequiredScopeIsNotNull() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) // non expired credentials insertTestCredentials(false, true, true, expiresAt, null) // "scope" is not set val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS + ONE_HOUR_SECONDS * 1000) @@ -840,7 +990,11 @@ 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, + callback + ) // minTTL of 0 seconds (default) verify(request) .addParameter(eq("scope"), eq("different scope")) verify(callback).onSuccess( @@ -884,6 +1038,9 @@ public class SecureCredentialsManagerTest { @Test public fun shouldRenewExpiredCredentialsWhenScopeHasChanged() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } val expiresAt = Date(CredentialsMock.CURRENT_TIME_MS) // current time means expired credentials insertTestCredentials(false, true, true, expiresAt, "scope") @@ -903,7 +1060,11 @@ 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, + callback + ) // minTTL of 0 seconds (default) verify(request) .addParameter(eq("scope"), eq("different scope")) verify(callback).onSuccess( @@ -947,6 +1108,9 @@ public class SecureCredentialsManagerTest { @Test public fun shouldNotHaveCredentialsWhenAccessTokenWillExpireAndNoRefreshTokenIsAvailable() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS Mockito.`when`(storage.retrieveLong("com.auth0.credentials_expires_at")) .thenReturn(expirationTime) @@ -961,6 +1125,9 @@ public class SecureCredentialsManagerTest { @Test public fun shouldGetAndSuccessfullyRenewExpiredCredentials() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } val expiresAt = Date(CredentialsMock.CURRENT_TIME_MS) // current time means expired credentials insertTestCredentials(false, true, true, expiresAt, "scope") @@ -979,8 +1146,8 @@ public class SecureCredentialsManagerTest { Mockito.`when`(request.execute()).thenReturn(expectedCredentials) // Trigger success - val renewedCredentials = - Credentials("newId", "newAccess", "newType", null, newDate, "newScope") +// val renewedCredentials = +// Credentials("newId", "newAccess", "newType", null, newDate, "newScope") val expectedJson = gson.toJson(expectedCredentials) Mockito.`when`(crypto.encrypt(expectedJson.toByteArray())) .thenReturn(expectedJson.toByteArray()) @@ -1026,6 +1193,9 @@ public class SecureCredentialsManagerTest { @Test public fun shouldGetAndSuccessfullyRenewExpiredCredentialsWithRefreshTokenRotation() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } val expiresAt = Date(CredentialsMock.CURRENT_TIME_MS) insertTestCredentials(false, true, true, expiresAt, "scope") val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) @@ -1090,6 +1260,9 @@ public class SecureCredentialsManagerTest { @Test public fun shouldGetAndFailToRenewExpiredCredentials() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } val expiresAt = Date(CredentialsMock.CURRENT_TIME_MS) insertTestCredentials(false, true, true, expiresAt, "scope") Mockito.`when`( @@ -1120,6 +1293,105 @@ public class SecureCredentialsManagerTest { ) } + /** + * Testing that getCredentials execution from multiple threads via multiple instances of SecureCredentialsManager should trigger only one network request. + */ + @Test + public fun shouldSynchronizeGetCredentialsAccessAcrossThreadsAndInstances() { + + val expiredCredentials = Credentials( + "", + "accessToken", + "type", + "refreshToken", + Date(CredentialsMock.CURRENT_TIME_MS), + "scope" + ) + val renewedCredentials = + Credentials( + "newId", + "newAccess", + "newType", + "rotatedRefreshToken", + Date(CredentialsMock.ONE_HOUR_AHEAD_MS), + "newScope" + ) + Mockito.`when`( + client.renewAuth("refreshToken") + ).thenReturn(request) + Mockito.`when`(request.execute()).thenReturn(renewedCredentials) + val serialExecutor = Executors.newSingleThreadExecutor() + Mockito.`when`(auth0.executor).thenReturn(serialExecutor) + val executor: ExecutorService = Executors.newFixedThreadPool(5) + val latch = CountDownLatch(5) + val context: Context = + Robolectric.buildActivity(Activity::class.java).create().start().resume().get() + val storage = SharedPreferencesStorage( + context = context, + sharedPreferencesName = "com.auth0.android.storage.SecureCredentialsManagerTest" + ) + val cryptoMock = Mockito.mock(CryptoUtil::class.java) + Mockito.`when`(cryptoMock.encrypt(any())).thenAnswer { + val input = it.arguments[0] as ByteArray + input + } + Mockito.`when`(cryptoMock.decrypt(any())).thenAnswer { + val input = it.arguments[0] as ByteArray + input + } + val secureCredsManager = + SecureCredentialsManager(client, storage, cryptoMock, jwtDecoder, auth0.executor) + secureCredsManager.saveCredentials(expiredCredentials) + repeat(5) { + executor.submit { + val secureCredsManager = + SecureCredentialsManager( + client, + storage, + cryptoMock, + jwtDecoder, + auth0.executor, + ) + secureCredsManager.getCredentials(object : + Callback { + override fun onFailure(exception: CredentialsManagerException) { + throw exception + } + + override fun onSuccess(credentials: Credentials) { + // Verify all instances retrieved the same credentials + MatcherAssert.assertThat( + renewedCredentials.accessToken, + Is.`is`(credentials.accessToken) + ) + MatcherAssert.assertThat( + renewedCredentials.idToken, + Is.`is`(credentials.idToken) + ) + MatcherAssert.assertThat( + renewedCredentials.refreshToken, + Is.`is`(credentials.refreshToken) + ) + MatcherAssert.assertThat(renewedCredentials.type, Is.`is`(credentials.type)) + MatcherAssert.assertThat( + renewedCredentials.expiresAt, + Is.`is`(credentials.expiresAt) + ) + MatcherAssert.assertThat( + renewedCredentials.scope, + Is.`is`(credentials.scope) + ) + latch.countDown() + } + }) + } + } + latch.await() // Wait for all threads to finish + Mockito.verify(client, Mockito.times(1)) + .renewAuth(any()) // verify that api client's renewAuth is called only once + Mockito.verify(request, Mockito.times(1)).execute() // Verify single network request + } + /* * CLEAR Credentials tests */ @@ -1233,495 +1505,400 @@ 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) + 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) + + manager.getCredentials(callback) + verify(callback).onSuccess(credentialsCaptor.capture()) + 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")) } @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)) + 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(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 - @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)) + 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(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.")) } @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)) + public fun shouldNotGetCredentialsWhenBiometricHardwareUnavailable() { + 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`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onFailure( + CredentialsManagerException.BIOMETRIC_ERROR_HW_UNAVAILABLE + ) + } + manager.getCredentials(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.") + ) } @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)) + public fun shouldNotGetCredentialsWhenBiometricsUnableToProcess() { + 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`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onFailure( + CredentialsManagerException.BIOMETRIC_ERROR_UNABLE_TO_PROCESS + ) + } + manager.getCredentials(callback) + // Should fail because of unable to process + 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 sensor was unable to process the current image.") + ) } @Test - public fun shouldGetCredentialsAfterAuthenticationUsingActivityResultsAPI() { + public fun shouldNotGetCredentialsWhenBiometricsTimeout() { 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`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onFailure( + CredentialsManagerException.BIOMETRIC_ERROR_TIMEOUT + ) } + manager.getCredentials(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.") + ) + } - 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 shouldNotGetCredentialsWhenBiometricsFailedDueToNoSpaceOnDevice() { + 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`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onFailure( + CredentialsManagerException.BIOMETRIC_ERROR_NO_SPACE ) - - // 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() + // 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 (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 shouldNotGetCredentialsAfterCanceledAuthenticationUsingActivityResultsAPI() { + public fun shouldNotGetCredentialsWhenBiometricsFailedDueToCancellation() { 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 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) - } + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onFailure( + CredentialsManagerException.BIOMETRIC_ERROR_CANCELED + ) } + manager.getCredentials(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`("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.") + ) + } - 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 shouldNotGetCredentialsWhenBiometricsFailedDueToLockOut() { + 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`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onFailure( + CredentialsManagerException.BIOMETRIC_ERROR_LOCKOUT ) - - // 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()) + // Should fail because of lockout 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 user has been temporarily locked out, this occurs after 5 failed attempts and lasts for 30 seconds.") ) } @Test - public fun shouldNotThrowWhenRequiringAuthenticationAfterStartingTheActivity() { + public fun shouldNotGetCredentialsWhenBiometricsFailedDueToVendorError() { 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)) + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onFailure( + CredentialsManagerException.BIOMETRIC_ERROR_VENDOR + ) + } 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() + // 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.") ) - 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() { + public fun shouldNotGetCredentialsWhenBiometricsFailedDueToPermanentLockout() { 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)) + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onFailure( + CredentialsManagerException.BIOMETRIC_ERROR_LOCKOUT_PERMANENT + ) + } 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() + // 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`("Failed to authenticate because the user has been permanently locked 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 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)) + public fun shouldNotGetCredentialsWhenBiometricsFailedDueToCancellationByUser() { + 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`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onFailure( + CredentialsManagerException.BIOMETRIC_ERROR_USER_CANCELED + ) + } manager.getCredentials(callback) - - // Should fail because of expired credentials + // 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`("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 user canceled the operation.") + ) } @Test - public fun shouldNotGetCredentialsWhenCredentialsWereClearedBeforeContinuing() { + public fun shouldNotGetCredentialsWhenBiometricsFailedDueToNoBiometrics() { 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)) + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onFailure( + CredentialsManagerException.BIOMETRIC_ERROR_NO_BIOMETRICS + ) + } manager.getCredentials(callback) - val intentCaptor = ArgumentCaptor.forClass(Intent::class.java) - verify(activity) - .startActivityForResult(intentCaptor.capture(), eq(123)) - MatcherAssert.assertThat(intentCaptor.value, Is.`is`(confirmCredentialsIntent)) - - // 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)) + // 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`("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 user does not have any biometrics enrolled.") + ) } @Test - public fun shouldNotGetCredentialsAfterCanceledAuthentication() { + public fun shouldNotGetCredentialsWhenBiometricsFailedDueToHardwareNotPresent() { val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) insertTestCredentials(true, true, false, expiresAt, "scope") - - // 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)) + Mockito.`when`(storage.retrieveLong("com.auth0.credentials_expires_at")) + .thenReturn(expiresAt.time) + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onFailure( + CredentialsManagerException.BIOMETRIC_ERROR_HW_NOT_PRESENT + ) + } 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()) + // 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() { + public fun shouldNotGetCredentialsWhenBiometricsFailedBecauseUserPressedTheNegativeButton() { 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`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onFailure( + CredentialsManagerException.BIOMETRIC_ERROR_NEGATIVE_BUTTON + ) + } + manager.getCredentials(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)) + @Test + public fun shouldNotGetCredentialsWhenBiometricsFailedBecauseNoDeviceCredentials() { + 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`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onFailure( + CredentialsManagerException.BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL + ) + } manager.getCredentials(callback) - val intentCaptor = ArgumentCaptor.forClass(Intent::class.java) - verify(activity) - .startActivityForResult(intentCaptor.capture(), eq(100)) - MatcherAssert.assertThat(intentCaptor.value, Is.`is`(confirmCredentialsIntent)) + // 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.") + ) + } - // Continue after successful authentication - verifyNoMoreInteractions(callback) - val processed = manager.checkAuthenticationResult(123, Activity.RESULT_OK) - MatcherAssert.assertThat(processed, Is.`is`(false)) + @Test + public fun shouldNotGetCredentialsWhenFragmentActivityIsGarbageCollected() { + 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) + manager.clearFragmentActivity() + manager.getCredentials(callback) + // Should fail because no fragment activity + verify(callback).onFailure( + exceptionCaptor.capture() + ) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat( + exception.message, + Is.`is`("Cannot authenticate as the activity passed is null.") + ) } /* @@ -1729,7 +1906,16 @@ public class SecureCredentialsManagerTest { */ @Test public fun shouldUseCustomClock() { - val manager = SecureCredentialsManager(client, storage, crypto, jwtDecoder) { } + val manager = SecureCredentialsManager( + client, + storage, + crypto, + jwtDecoder, + auth0.executor, + weakFragmentActivity, + getAuthenticationOptions(), + 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,18 +1936,81 @@ public class SecureCredentialsManagerTest { @Test(expected = java.lang.IllegalArgumentException::class) public fun shouldUseCustomExecutorForGetCredentials() { - val manager = SecureCredentialsManager(client, storage, crypto, jwtDecoder) { - throw java.lang.IllegalArgumentException("Proper Executor Set") + val serialExecutor = object : ExecutorService { + override fun execute(command: Runnable?) { + throw IllegalArgumentException("Proper Executor Set") + } + + override fun shutdown() {} + override fun shutdownNow(): List = emptyList() + override fun isShutdown(): Boolean = false + override fun isTerminated(): Boolean = false + override fun awaitTermination(timeout: Long, unit: TimeUnit): Boolean = false + override fun submit(task: java.util.concurrent.Callable): java.util.concurrent.Future { + throw IllegalArgumentException("Proper Executor Set") + } + + override fun submit( + task: Runnable?, + result: T + ): java.util.concurrent.Future { + throw IllegalArgumentException("Proper Executor Set") + } + + override fun submit(task: Runnable?): java.util.concurrent.Future<*> { + throw IllegalArgumentException("Proper Executor Set") + } + + override fun invokeAll(tasks: Collection>?): List> { + throw IllegalArgumentException("Proper Executor Set") + } + + override fun invokeAll( + tasks: Collection>?, + timeout: Long, + unit: TimeUnit + ): List> { + throw IllegalArgumentException("Proper Executor Set") + } + + override fun invokeAny(tasks: Collection>?): T { + throw IllegalArgumentException("Proper Executor Set") + } + + override fun invokeAny( + tasks: Collection>?, + timeout: Long, + unit: TimeUnit + ): T { + throw IllegalArgumentException("Proper Executor Set") + } } + Mockito.`when`(auth0.executor).thenReturn(serialExecutor) + val manager = SecureCredentialsManager( + client, + storage, + crypto, + jwtDecoder, + auth0.executor, + weakFragmentActivity, + getAuthenticationOptions(), + factory + ) val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS Mockito.`when`(storage.retrieveLong("com.auth0.credentials_expires_at")) .thenReturn(expirationTime) 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) { } - }) + manager.continueGetCredentials( + null, + 0, + emptyMap(), + emptyMap(), + false, + object : Callback { + override fun onSuccess(result: Credentials) {} + override fun onFailure(error: CredentialsManagerException) {} + }) } @Test @@ -1792,11 +2041,13 @@ public class SecureCredentialsManagerTest { val expectedJson = gson.toJson(expectedCredentials) Mockito.`when`(crypto.encrypt(expectedJson.toByteArray())) .thenReturn(expectedJson.toByteArray()) - manager.getCredentials( - scope = "some changed scope to trigger refresh", - minTtl = 0, - parameters = parameters, - callback = callback + manager.continueGetCredentials( + "some changed scope to trigger refresh", + 0, + parameters, + emptyMap(), + false, + callback ) verify(request).addParameters(parameters) @@ -1831,10 +2082,11 @@ public class SecureCredentialsManagerTest { val expectedJson = gson.toJson(expectedCredentials) Mockito.`when`(crypto.encrypt(expectedJson.toByteArray())) .thenReturn(expectedJson.toByteArray()) - manager.getCredentials( + manager.continueGetCredentials( scope = "scope", minTtl = 0, parameters = parameters, + headers = emptyMap(), forceRefresh = true, callback = callback ) @@ -1842,12 +2094,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 +2117,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.continueGetCredentials( + "scope", 0, emptyMap(), + emptyMap(), false, callback ) @@ -1876,13 +2139,6 @@ public class SecureCredentialsManagerTest { MatcherAssert.assertThat(retrievedCredentials.scope, Is.`is`("scope")) } - @Test - public fun shouldBeMarkedSynchronous(){ - val method = - SecureCredentialsManager::class.java.getMethod("saveCredentials", Credentials::class.java) - assertTrue(Modifier.isSynchronized(method.modifiers)) - } - /* * Helper methods */ @@ -1927,4 +2183,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 { + setTitle(title) + setSubTitle(subtitle) + setDescription(description) + setNegativeButtonText(negativeButtonText) + setAuthenticationLevel(authenticator) + setDeviceCredentialFallback(enableDeviceCredentialFallback) + } + return builder.build() + } } \ No newline at end of file diff --git a/auth0/src/test/java/com/auth0/android/management/UsersAPIClientTest.kt b/auth0/src/test/java/com/auth0/android/management/UsersAPIClientTest.kt index 6cefe080a..728e026d0 100755 --- a/auth0/src/test/java/com/auth0/android/management/UsersAPIClientTest.kt +++ b/auth0/src/test/java/com/auth0/android/management/UsersAPIClientTest.kt @@ -51,7 +51,7 @@ public class UsersAPIClientTest { public fun setUp() { mockAPI = UsersAPIMockServer() val domain = mockAPI.domain - val auth0 = Auth0(CLIENT_ID, domain, domain) + val auth0 = Auth0.getInstance(CLIENT_ID, domain, domain) auth0.networkingClient = testClient client = UsersAPIClient(auth0, TOKEN_PRIMARY) gson = GsonBuilder().serializeNulls().create() @@ -64,7 +64,7 @@ public class UsersAPIClientTest { @Test public fun shouldUseCustomNetworkingClient() { - val account = Auth0("client-id", "https://tenant.auth0.com/") + val account = Auth0.getInstance("client-id", "https://tenant.auth0.com/") val jsonResponse = """{"id": "undercover"}""" val inputStream: InputStream = ByteArrayInputStream(jsonResponse.toByteArray()) val response = ServerResponse(200, inputStream, emptyMap()) @@ -90,7 +90,7 @@ public class UsersAPIClientTest { public fun shouldSetAuth0UserAgentIfPresent() { val auth0UserAgent: Auth0UserAgent = mock() val factory: RequestFactory = mock() - val account = Auth0(CLIENT_ID, DOMAIN) + val account = Auth0.getInstance(CLIENT_ID, DOMAIN) whenever(auth0UserAgent.value).thenReturn("the-user-agent-data") account.auth0UserAgent = auth0UserAgent @@ -101,7 +101,7 @@ public class UsersAPIClientTest { @Test public fun shouldCreateClientWithAccountInfo() { - val client = UsersAPIClient(Auth0(CLIENT_ID, DOMAIN), TOKEN_PRIMARY) + val client = UsersAPIClient(Auth0.getInstance(CLIENT_ID, DOMAIN), TOKEN_PRIMARY) assertThat(client, Matchers.`is`(notNullValue())) assertThat(client.clientId, Matchers.equalTo(CLIENT_ID)) assertThat(client.baseURL, Matchers.equalTo("https://$DOMAIN/")) @@ -127,7 +127,7 @@ public class UsersAPIClientTest { whenever(context.getString(eq(222))).thenReturn(CLIENT_ID) whenever(context.getString(eq(333))).thenReturn(DOMAIN) - val client = UsersAPIClient(Auth0(context), TOKEN_PRIMARY) + val client = UsersAPIClient(Auth0.getInstance(context), TOKEN_PRIMARY) assertThat(client, Matchers.`is`(notNullValue())) assertThat(client.clientId, Matchers.`is`(CLIENT_ID)) assertThat(client.baseURL, Matchers.equalTo("https://$DOMAIN/")) diff --git a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt index 5f5079615..dbda94350 100644 --- a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt +++ b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt @@ -76,7 +76,8 @@ public class WebAuthProviderTest { public fun setUp() { MockitoAnnotations.openMocks(this) activity = Mockito.spy(Robolectric.buildActivity(Activity::class.java).get()) - account = Auth0(JwtTestUtils.EXPECTED_AUDIENCE, JwtTestUtils.EXPECTED_BASE_DOMAIN) + account = + Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, JwtTestUtils.EXPECTED_BASE_DOMAIN) account.networkingClient = SSLTestUtils.testClient //Next line is needed to avoid CustomTabService from being bound to Test environment @@ -1013,7 +1014,7 @@ public class WebAuthProviderTest { val mockAPI = AuthenticationAPIMockServer() mockAPI.willReturnValidJsonWebKeys() val authCallback = mock>() - val proxyAccount: Auth0 = Auth0(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) + val proxyAccount: Auth0 = Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) proxyAccount.networkingClient = SSLTestUtils.testClient login(proxyAccount) .withPKCE(pkce) @@ -1086,7 +1087,8 @@ public class WebAuthProviderTest { val authCallback = mock>() // 1. start the webauth flow. the browser would open - val proxyAccount = Auth0(JwtTestUtils.EXPECTED_AUDIENCE, JwtTestUtils.EXPECTED_BASE_DOMAIN) + val proxyAccount = + Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, JwtTestUtils.EXPECTED_BASE_DOMAIN) proxyAccount.networkingClient = networkingClient login(proxyAccount) .start(activity, authCallback) @@ -1201,7 +1203,7 @@ public class WebAuthProviderTest { val mockAPI = AuthenticationAPIMockServer() mockAPI.willReturnValidJsonWebKeys() val authCallback = mock>() - val proxyAccount: Auth0 = Auth0(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) + val proxyAccount: Auth0 = Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) proxyAccount.networkingClient = SSLTestUtils.testClient login(proxyAccount) .withPKCE(pkce) @@ -1456,7 +1458,7 @@ public class WebAuthProviderTest { val mockAPI = AuthenticationAPIMockServer() mockAPI.willReturnEmptyJsonWebKeys() val authCallback = mock>() - val proxyAccount: Auth0 = Auth0(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) + val proxyAccount: Auth0 = Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) proxyAccount.networkingClient = SSLTestUtils.testClient login(proxyAccount) .withState("1234567890") @@ -1522,7 +1524,7 @@ public class WebAuthProviderTest { val mockAPI = AuthenticationAPIMockServer() mockAPI.willReturnInvalidRequest() val authCallback = mock>() - val proxyAccount: Auth0 = Auth0(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) + val proxyAccount: Auth0 = Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) proxyAccount.networkingClient = SSLTestUtils.testClient login(proxyAccount) .withState("1234567890") @@ -1588,7 +1590,7 @@ public class WebAuthProviderTest { val mockAPI = AuthenticationAPIMockServer() mockAPI.willReturnValidJsonWebKeys() val authCallback = mock>() - val proxyAccount: Auth0 = Auth0(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) + val proxyAccount: Auth0 = Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) proxyAccount.networkingClient = SSLTestUtils.testClient login(proxyAccount) .withState("1234567890") @@ -1653,7 +1655,7 @@ public class WebAuthProviderTest { val mockAPI = AuthenticationAPIMockServer() mockAPI.willReturnValidJsonWebKeys() val authCallback = mock>() - val proxyAccount: Auth0 = Auth0(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) + val proxyAccount: Auth0 = Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) proxyAccount.networkingClient = SSLTestUtils.testClient login(proxyAccount) .withState("1234567890") @@ -1702,7 +1704,7 @@ public class WebAuthProviderTest { // if specifying a null issuer for token verification, should use the domain URL of the account val mockAPI = AuthenticationAPIMockServer() mockAPI.willReturnValidJsonWebKeys() - val proxyAccount: Auth0 = Auth0(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) + val proxyAccount: Auth0 = Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) proxyAccount.networkingClient = SSLTestUtils.testClient val authCallback = mock>() login(proxyAccount) @@ -1773,7 +1775,7 @@ public class WebAuthProviderTest { val mockAPI = AuthenticationAPIMockServer() mockAPI.willReturnValidJsonWebKeys() val authCallback = mock>() - val proxyAccount: Auth0 = Auth0(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) + val proxyAccount: Auth0 = Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) proxyAccount.networkingClient = SSLTestUtils.testClient login(proxyAccount) .withIdTokenVerificationIssuer("https://some.different.issuer/") @@ -1842,7 +1844,7 @@ public class WebAuthProviderTest { `when`(pkce.codeChallenge).thenReturn("challenge") val mockAPI = AuthenticationAPIMockServer() mockAPI.willReturnValidJsonWebKeys() - val proxyAccount: Auth0 = Auth0(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) + val proxyAccount: Auth0 = Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) proxyAccount.networkingClient = SSLTestUtils.testClient val authCallback = mock>() login(proxyAccount) @@ -2005,7 +2007,7 @@ public class WebAuthProviderTest { val mockAPI = AuthenticationAPIMockServer() mockAPI.willReturnValidJsonWebKeys() val authCallback = mock>() - val proxyAccount: Auth0 = Auth0(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) + val proxyAccount: Auth0 = Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) proxyAccount.networkingClient = SSLTestUtils.testClient login(proxyAccount) .withState("state") @@ -2076,7 +2078,7 @@ public class WebAuthProviderTest { val mockAPI = AuthenticationAPIMockServer() mockAPI.willReturnValidJsonWebKeys() val authCallback = mock>() - val proxyAccount: Auth0 = Auth0(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) + val proxyAccount: Auth0 = Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) proxyAccount.networkingClient = SSLTestUtils.testClient login(proxyAccount) .withState("state") @@ -2131,7 +2133,7 @@ public class WebAuthProviderTest { val mockAPI = AuthenticationAPIMockServer() mockAPI.willReturnValidJsonWebKeys() val authCallback = mock>() - val proxyAccount: Auth0 = Auth0(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) + val proxyAccount: Auth0 = Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) proxyAccount.networkingClient = SSLTestUtils.testClient login(proxyAccount) .withState("state") @@ -2596,8 +2598,10 @@ public class WebAuthProviderTest { @Test @ExperimentalCoroutinesApi public fun shouldResumeLogoutSuccessfullyWithCoroutines(): Unit = runTest { - val job = launch { logout(account) - .await(activity, Dispatchers.Unconfined) } + val job = launch { + logout(account) + .await(activity, Dispatchers.Unconfined) + } advanceUntilIdle() verify(activity).startActivity(intentCaptor.capture()) val uri = diff --git a/auth0/src/test/java/com/auth0/android/request/DefaultClientTest.kt b/auth0/src/test/java/com/auth0/android/request/DefaultClientTest.kt index 4f1e19020..a30ca88bc 100644 --- a/auth0/src/test/java/com/auth0/android/request/DefaultClientTest.kt +++ b/auth0/src/test/java/com/auth0/android/request/DefaultClientTest.kt @@ -306,6 +306,7 @@ public class DefaultClientTest { readTimeout = 10, connectTimeout = 10, enableLogging = false, + gson = gson, sslSocketFactory = SSLTestUtils.clientCertificates.sslSocketFactory(), trustManager = SSLTestUtils.clientCertificates.trustManager ) diff --git a/auth0/src/test/java/com/auth0/android/request/internal/CredentialsDeserializerMock.kt b/auth0/src/test/java/com/auth0/android/request/internal/CredentialsDeserializerMock.kt index 52d745462..b2ade84c6 100644 --- a/auth0/src/test/java/com/auth0/android/request/internal/CredentialsDeserializerMock.kt +++ b/auth0/src/test/java/com/auth0/android/request/internal/CredentialsDeserializerMock.kt @@ -15,7 +15,7 @@ internal class CredentialsDeserializerMock : CredentialsDeserializer() { recoveryCode: String? ): Credentials { val credentials = - CredentialsMock(idToken, accessToken, type, refreshToken, expiresAt, scope) + CredentialsMock.create(idToken, accessToken, type, refreshToken, expiresAt, scope) credentials.recoveryCode = recoveryCode return credentials } diff --git a/auth0/src/test/java/com/auth0/android/request/internal/CredentialsGsonTest.kt b/auth0/src/test/java/com/auth0/android/request/internal/CredentialsGsonTest.kt index 6813a2a54..d10df32a5 100755 --- a/auth0/src/test/java/com/auth0/android/request/internal/CredentialsGsonTest.kt +++ b/auth0/src/test/java/com/auth0/android/request/internal/CredentialsGsonTest.kt @@ -120,7 +120,7 @@ public class CredentialsGsonTest : GsonBaseTest() { val expiresAt = Date(CredentialsMock.CURRENT_TIME_MS + 123456 * 1000) val expectedExpiresAt = formatDate(expiresAt) val expiresInCredentials: Credentials = - CredentialsMock("id", "access", "ty", "refresh", expiresAt, null) + CredentialsMock.create("id", "access", "ty", "refresh", expiresAt, null) val expiresInJson = gson.toJson(expiresInCredentials) MatcherAssert.assertThat(expiresInJson, CoreMatchers.containsString("\"id_token\":\"id\"")) MatcherAssert.assertThat( @@ -149,7 +149,7 @@ public class CredentialsGsonTest : GsonBaseTest() { CoreMatchers.not(CoreMatchers.containsString("\"scope\"")) ) val expiresAtCredentials: Credentials = - CredentialsMock("id", "access", "ty", "refresh", expiresAt, "openid") + CredentialsMock.create("id", "access", "ty", "refresh", expiresAt, "openid") val expiresAtJson = gson.toJson(expiresAtCredentials) MatcherAssert.assertThat(expiresAtJson, CoreMatchers.containsString("\"id_token\":\"id\"")) MatcherAssert.assertThat( diff --git a/auth0/src/test/java/com/auth0/android/request/internal/ResponseUtilsTest.kt b/auth0/src/test/java/com/auth0/android/request/internal/ResponseUtilsTest.kt new file mode 100644 index 000000000..14dd8855a --- /dev/null +++ b/auth0/src/test/java/com/auth0/android/request/internal/ResponseUtilsTest.kt @@ -0,0 +1,27 @@ +package com.auth0.android.request.internal + +import junit.framework.TestCase.assertTrue +import org.hamcrest.CoreMatchers +import org.hamcrest.MatcherAssert +import org.junit.Test +import java.net.SocketException +import java.net.SocketTimeoutException +import java.net.UnknownHostException + +public class ResponseUtilsTest { + + @Test + public fun testIsNetworkErrorWhenSocketExceptionOccurs() { + MatcherAssert.assertThat(ResponseUtils.isNetworkError(SocketException()), CoreMatchers.`is`(true)) + } + + @Test + public fun testIsNetworkErrorWhenSocketTimeoutExceptionOccurs() { + MatcherAssert.assertThat(ResponseUtils.isNetworkError(SocketTimeoutException()), CoreMatchers.`is`(true)) + } + + @Test + public fun testIsNetworkErrorWhenUnknownHostExceptionOccurs() { + MatcherAssert.assertThat(ResponseUtils.isNetworkError(UnknownHostException()), CoreMatchers.`is`(true)) + } +} \ No newline at end of file diff --git a/auth0/src/test/java/com/auth0/android/result/CredentialsMock.kt b/auth0/src/test/java/com/auth0/android/result/CredentialsMock.kt index c60371c5c..50dd8b41b 100644 --- a/auth0/src/test/java/com/auth0/android/result/CredentialsMock.kt +++ b/auth0/src/test/java/com/auth0/android/result/CredentialsMock.kt @@ -2,17 +2,7 @@ package com.auth0.android.result import java.util.* -public class CredentialsMock( - idToken: String, - accessToken: String, - type: String, - refreshToken: String?, - expiresAt: Date, - scope: String? -) : Credentials(idToken, accessToken, type, refreshToken, expiresAt, scope) { - - override val currentTimeInMillis: Long - get() = CURRENT_TIME_MS +public class CredentialsMock { public companion object { @JvmField @@ -26,5 +16,16 @@ public class CredentialsMock( cal.timeZone = TimeZone.getTimeZone("UTC") return cal.timeInMillis } + + public fun create( + idToken: String, + accessToken: String, + type: String, + refreshToken: String?, + expiresAt: Date, + scope: String? + ): Credentials { + return Credentials(idToken, accessToken, type, refreshToken, expiresAt, scope) + } } } \ No newline at end of file diff --git a/auth0/src/test/java/com/auth0/android/result/CredentialsTest.kt b/auth0/src/test/java/com/auth0/android/result/CredentialsTest.kt index cd40d358d..ac38dc36b 100644 --- a/auth0/src/test/java/com/auth0/android/result/CredentialsTest.kt +++ b/auth0/src/test/java/com/auth0/android/result/CredentialsTest.kt @@ -6,10 +6,10 @@ import org.hamcrest.Matchers import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -import java.text.SimpleDateFormat import java.util.* -private val idToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwOi8vbXktZG9tYWluLmF1dGgwLmNvbSIsInN1YiI6ImF1dGgwfDEyMzQ1NiIsImF1ZCI6Im15X2NsaWVudF9pZCIsImV4cCI6MTMxMTI4MTk3MCwiaWF0IjoxMzExMjgwOTcwLCJuYW1lIjoiSmFuZSBEb2UiLCJnaXZlbl9uYW1lIjoiSmFuZSIsImZhbWlseV9uYW1lIjoiRG9lIiwiZ2VuZGVyIjoiZmVtYWxlIiwiYmlydGhkYXRlIjoiMDAwMC0xMC0zMSIsImVtYWlsIjoiamFuZWRvZUBleGFtcGxlLmNvbSIsInBpY3R1cmUiOiJodHRwOi8vZXhhbXBsZS5jb20vamFuZWRvZS9tZS5qcGcifQ.FKw0UVWANEqibD9VTC9WLzstlyc_IRnyPSpUMDP3hKc" +private val idToken = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwOi8vbXktZG9tYWluLmF1dGgwLmNvbSIsInN1YiI6ImF1dGgwfDEyMzQ1NiIsImF1ZCI6Im15X2NsaWVudF9pZCIsImV4cCI6MTMxMTI4MTk3MCwiaWF0IjoxMzExMjgwOTcwLCJuYW1lIjoiSmFuZSBEb2UiLCJnaXZlbl9uYW1lIjoiSmFuZSIsImZhbWlseV9uYW1lIjoiRG9lIiwiZ2VuZGVyIjoiZmVtYWxlIiwiYmlydGhkYXRlIjoiMDAwMC0xMC0zMSIsImVtYWlsIjoiamFuZWRvZUBleGFtcGxlLmNvbSIsInBpY3R1cmUiOiJodHRwOi8vZXhhbXBsZS5jb20vamFuZWRvZS9tZS5qcGcifQ.FKw0UVWANEqibD9VTC9WLzstlyc_IRnyPSpUMDP3hKc" @RunWith(RobolectricTestRunner::class) public class CredentialsTest { @@ -17,7 +17,7 @@ public class CredentialsTest { public fun shouldCreate() { val date = Date() val credentials: Credentials = - CredentialsMock("idToken", "accessToken", "type", "refreshToken", date, "scope") + CredentialsMock.create("idToken", "accessToken", "type", "refreshToken", date, "scope") MatcherAssert.assertThat(credentials.idToken, Matchers.`is`("idToken")) MatcherAssert.assertThat(credentials.accessToken, Matchers.`is`("accessToken")) MatcherAssert.assertThat(credentials.type, Matchers.`is`("type")) @@ -38,7 +38,7 @@ public class CredentialsTest { public fun shouldGetRecoveryCode() { val date = Date() val credentials: Credentials = - CredentialsMock("idToken", "accessToken", "type", "refreshToken", date, "scope") + CredentialsMock.create("idToken", "accessToken", "type", "refreshToken", date, "scope") credentials.recoveryCode = "recoveryCode" MatcherAssert.assertThat(credentials.recoveryCode, Matchers.`is`("recoveryCode")) } @@ -47,12 +47,15 @@ public class CredentialsTest { public fun shouldGetUser() { val date = Date() val credentials: Credentials = - CredentialsMock(idToken, "accessToken", "type", "refreshToken", date, "scope") + CredentialsMock.create(idToken, "accessToken", "type", "refreshToken", date, "scope") MatcherAssert.assertThat(credentials.user.getId(), Matchers.`is`("auth0|123456")) MatcherAssert.assertThat(credentials.user.name, Matchers.`is`("Jane Doe")) MatcherAssert.assertThat(credentials.user.givenName, Matchers.`is`("Jane")) MatcherAssert.assertThat(credentials.user.familyName, Matchers.`is`("Doe")) - MatcherAssert.assertThat(credentials.user.pictureURL, Matchers.`is`("http://example.com/janedoe/me.jpg")) + MatcherAssert.assertThat( + credentials.user.pictureURL, + Matchers.`is`("http://example.com/janedoe/me.jpg") + ) MatcherAssert.assertThat(credentials.user.email, Matchers.`is`("janedoe@example.com")) } @@ -60,7 +63,7 @@ public class CredentialsTest { public fun shouldNotSerializeUser() { val date = Date() val credentials: Credentials = - CredentialsMock(idToken, "accessToken", "type", "refreshToken", date, "scope") + CredentialsMock.create(idToken, "accessToken", "type", "refreshToken", date, "scope") MatcherAssert.assertThat(credentials.user.getId(), Matchers.`is`("auth0|123456")) val json = gson.toJson(credentials, Credentials::class.java) MatcherAssert.assertThat(json, Matchers.not(Matchers.containsString("auth0|123456"))) @@ -72,7 +75,10 @@ public class CredentialsTest { public fun shouldNotPrintCredentials() { val date = Date() val credentials: Credentials = - CredentialsMock(idToken, "accessToken", "type", "refreshToken", date, "scope") - MatcherAssert.assertThat(credentials.toString(), Matchers.`is`("Credentials(idToken='xxxxx', accessToken='xxxxx', type='type', refreshToken='xxxxx', expiresAt='$date', scope='scope')")) + CredentialsMock.create(idToken, "accessToken", "type", "refreshToken", date, "scope") + MatcherAssert.assertThat( + credentials.toString(), + Matchers.`is`("Credentials(idToken='xxxxx', accessToken='xxxxx', type='type', refreshToken='xxxxx', expiresAt='$date', scope='scope')") + ) } } \ No newline at end of file diff --git a/auth0/src/test/java/com/auth0/android/util/SSLTestUtils.kt b/auth0/src/test/java/com/auth0/android/util/SSLTestUtils.kt index ac0128cfa..542a8e4b6 100644 --- a/auth0/src/test/java/com/auth0/android/util/SSLTestUtils.kt +++ b/auth0/src/test/java/com/auth0/android/util/SSLTestUtils.kt @@ -1,6 +1,7 @@ package com.auth0.android.util import com.auth0.android.request.DefaultClient +import com.auth0.android.request.internal.GsonProvider import okhttp3.mockwebserver.MockWebServer import okhttp3.tls.HandshakeCertificates import okhttp3.tls.HeldCertificate @@ -35,6 +36,7 @@ internal object SSLTestUtils { readTimeout = 10, connectTimeout = 10, enableLogging = false, + gson = GsonProvider.gson, sslSocketFactory = clientCertificates.sslSocketFactory(), trustManager = clientCertificates.trustManager ) diff --git a/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt b/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt index c73223d15..08bf0ac54 100644 --- a/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt +++ b/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt @@ -8,7 +8,10 @@ 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.CredentialsManager 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 @@ -33,7 +36,7 @@ class DatabaseLoginFragment : Fragment() { private val account: Auth0 by lazy { // -- REPLACE this credentials with your own Auth0 app credentials! - val account = Auth0( + val account = Auth0.getInstance( getString(R.string.com_auth0_client_id), getString(R.string.com_auth0_domain) ) @@ -50,12 +53,27 @@ 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) + val manager = SecureCredentialsManager( + requireContext(), account, storage, requireActivity(), + localAuthenticationOptions + ) manager } + private val credentialsManager: CredentialsManager by lazy { + val storage = SharedPreferencesStorage(requireContext()) + val manager = CredentialsManager(authenticationApiClient, storage) + manager + } + + private val localAuthenticationOptions = + LocalAuthenticationOptions.Builder().setTitle("Biometric").setDescription("description") + .setAuthenticationLevel(AuthenticationLevel.STRONG).setNegativeButtonText("Cancel") + .setDeviceCredentialFallback(true) + .build() + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -95,6 +113,9 @@ class DatabaseLoginFragment : Fragment() { binding.btGetCredentials.setOnClickListener { getCreds() } + binding.getCredentialsSecure.setOnClickListener { + getCredsSecure() + } binding.btGetCredentialsAsync.setOnClickListener { launchAsync { getCredsAsync() @@ -121,11 +142,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(), @@ -169,6 +191,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}", @@ -197,7 +220,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() @@ -235,7 +258,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() @@ -247,24 +270,67 @@ class DatabaseLoginFragment : Fragment() { } private fun getCreds() { - credentialsManager.getCredentials(object : Callback { - override fun onSuccess(result: Credentials) { - Snackbar.make( - requireView(), - "Got credentials - ${result.accessToken}", - Snackbar.LENGTH_LONG - ).show() - } + credentialsManager.getCredentials( + 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() + } + }) + } + + private fun getCredsSecure() { + secureCredentialsManager.getCredentials( + 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") + } + // ... 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}", @@ -276,14 +342,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(), @@ -291,8 +363,9 @@ class DatabaseLoginFragment : Fragment() { Snackbar.LENGTH_LONG ).show() } - }) + }) } + override fun onFailure(error: CredentialsManagerException) { Snackbar.make(requireView(), "${error.message}", Snackbar.LENGTH_LONG).show() } @@ -321,14 +394,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(), @@ -336,8 +415,9 @@ class DatabaseLoginFragment : Fragment() { Snackbar.LENGTH_LONG ).show() } - }) + }) } + override fun onFailure(error: CredentialsManagerException) { Snackbar.make(requireView(), "${error.message}", Snackbar.LENGTH_LONG).show() } diff --git a/sample/src/main/res/layout/fragment_database_login.xml b/sample/src/main/res/layout/fragment_database_login.xml index a63f10047..af19b8b44 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" /> +