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 3afaae7b1..e56c2d87b 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 @@ -1,6 +1,7 @@ package com.auth0.android.authentication.storage import android.text.TextUtils +import android.util.Log import androidx.annotation.VisibleForTesting import com.auth0.android.authentication.AuthenticationAPIClient import com.auth0.android.authentication.AuthenticationException @@ -110,6 +111,17 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting error ) ) + } catch (exception: RuntimeException) { + Log.e( + TAG, + "Caught unexpected exceptions while fetching sso token ${exception.stackTraceToString()}" + ) + callback.onFailure( + CredentialsManagerException( + CredentialsManagerException.Code.UNKNOWN_ERROR, + exception + ) + ) } } } @@ -442,6 +454,20 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting exception, error ) ) + } catch (exception: RuntimeException) { + /** + * Catching any unexpected runtime errors in the token renewal flow + */ + Log.e( + TAG, + "Caught unexpected exceptions for token renewal ${exception.stackTraceToString()}" + ) + callback.onFailure( + CredentialsManagerException( + CredentialsManagerException.Code.UNKNOWN_ERROR, + exception + ) + ) } } } @@ -527,5 +553,6 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting // 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" + private val TAG = CredentialsManager::class.java.simpleName } } \ No newline at end of file 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 10210e286..8f8a981fb 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 @@ -46,6 +46,7 @@ public class CredentialsManagerException : NO_NETWORK, API_ERROR, SSO_EXCHANGE_FAILED, + UNKNOWN_ERROR } private var code: Code? @@ -146,6 +147,8 @@ public class CredentialsManagerException : public val SSO_EXCHANGE_FAILED: CredentialsManagerException = CredentialsManagerException(Code.SSO_EXCHANGE_FAILED) + public val UNKNOWN_ERROR: CredentialsManagerException = CredentialsManagerException(Code.UNKNOWN_ERROR) + private fun getMessage(code: Code): String { return when (code) { @@ -191,6 +194,7 @@ public class CredentialsManagerException : Code.NO_NETWORK -> "Failed to execute the network request." Code.API_ERROR -> "An error occurred while processing the request." Code.SSO_EXCHANGE_FAILED ->"The exchange of the refresh token for SSO credentials failed." + Code.UNKNOWN_ERROR -> "An unknown error has occurred while fetching the token. Please check the error cause for more details." } } } 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 feaf17296..6b4449e0d 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 @@ -192,6 +192,17 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT CredentialsManagerException.Code.STORE_FAILED, error ) callback.onFailure(exception) + } catch (exception: RuntimeException) { + Log.e( + TAG, + "Caught unexpected exceptions while fetching sso token ${exception.stackTraceToString()}" + ) + callback.onFailure( + CredentialsManagerException( + CredentialsManagerException.Code.UNKNOWN_ERROR, + exception + ) + ) } } } @@ -680,6 +691,21 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT ) ) return@execute + } catch (exception: RuntimeException) { + /** + * Catching any unexpected runtime errors in the token renewal flow + */ + Log.e( + TAG, + "Caught unexpected exceptions for token renewal ${exception.stackTraceToString()}" + ) + callback.onFailure( + CredentialsManagerException( + CredentialsManagerException.Code.UNKNOWN_ERROR, + exception + ) + ) + return@execute } try { 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 b12b23601..5105c3142 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 @@ -328,6 +328,31 @@ public class CredentialsManagerTest { ) } + @Test + public fun shouldFailOnGetNewSSOCredentialsWhenUnexpectedErrorOccurs() { + Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken") + Mockito.`when`( + client.ssoExchange("refreshToken") + ).thenReturn(SSOCredentialsRequest) + //Trigger failure + val runtimeException = RuntimeException( + "unexpected_error" + ) + Mockito.`when`(SSOCredentialsRequest.execute()).thenThrow(runtimeException) + manager.getSsoCredentials(ssoCallback) + verify(ssoCallback).onFailure( + exceptionCaptor.capture() + ) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat(exception, Is.`is`(CredentialsManagerException.UNKNOWN_ERROR)) + MatcherAssert.assertThat(exception.cause, Is.`is`(runtimeException)) + MatcherAssert.assertThat( + exception.message, + Is.`is`("An unknown error has occurred while fetching the token. Please check the error cause for more details.") + ) + } + @Test @ExperimentalCoroutinesApi public fun shouldFailOnAwaitSSOCredentialsWhenNoRefreshTokenWasSaved(): Unit = runTest { @@ -1122,6 +1147,46 @@ public class CredentialsManagerTest { ) } + @Test + public fun shouldGetAndFailToRenewExpiredCredentialsWhenAnyUnexpectedErrorOccurs() { + Mockito.`when`(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken") + Mockito.`when`(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken") + Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken") + Mockito.`when`(storage.retrieveString("com.auth0.token_type")).thenReturn("type") + val expirationTime = CredentialsMock.CURRENT_TIME_MS //Same as current time --> expired + Mockito.`when`(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime) + Mockito.`when`(storage.retrieveLong("com.auth0.cache_expires_at")) + .thenReturn(expirationTime) + Mockito.`when`(storage.retrieveString("com.auth0.scope")).thenReturn("scope") + Mockito.`when`( + client.renewAuth("refreshToken") + ).thenReturn(request) + //Trigger failure + val runtimeException = NullPointerException("Something went wrong") + Mockito.`when`(request.execute()).thenThrow(runtimeException) + manager.getCredentials(callback) + verify(storage, never()) + .store(ArgumentMatchers.anyString(), ArgumentMatchers.anyInt()) + verify(storage, never()) + .store(ArgumentMatchers.anyString(), ArgumentMatchers.anyLong()) + verify(storage, never()) + .store(ArgumentMatchers.anyString(), ArgumentMatchers.anyString()) + verify(storage, never()) + .store(ArgumentMatchers.anyString(), ArgumentMatchers.anyBoolean()) + verify(storage, never()).remove(ArgumentMatchers.anyString()) + verify(callback).onFailure( + exceptionCaptor.capture() + ) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat(exception, Is.`is`(CredentialsManagerException.UNKNOWN_ERROR)) + MatcherAssert.assertThat(exception.cause, Is.`is`(runtimeException)) + MatcherAssert.assertThat( + exception.message, + Is.`is`("An unknown error has occurred while fetching the token. Please check the error cause for more details.") + ) + } + @Test public fun shouldClearCredentials() { manager.clearCredentials() 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 42866e796..19ad357bd 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 @@ -378,6 +378,37 @@ public class SecureCredentialsManagerTest { ) } + @Test + public fun shouldFailWhenUnexpectedErrorOccursOnGetSSOCredentials() { + val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) + Mockito.`when`(client.ssoExchange("refreshToken")) + .thenReturn(SSOCredentialsRequest) + insertTestCredentials( + hasIdToken = true, + hasAccessToken = true, + hasRefreshToken = true, + willExpireAt = expiresAt, + scope = "scope" + ) + //Trigger failure + val runtimeException = RuntimeException( + "runtime exception" + ) + Mockito.`when`(SSOCredentialsRequest.execute()).thenThrow(runtimeException) + manager.getSsoCredentials(ssoCallback) + verify(ssoCallback).onFailure( + exceptionCaptor.capture() + ) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat(exception.cause, Is.`is`(runtimeException)) + MatcherAssert.assertThat(exception, Is.`is`(CredentialsManagerException.UNKNOWN_ERROR)) + MatcherAssert.assertThat( + exception.message, + Is.`is`("An unknown error has occurred while fetching the token. Please check the error cause for more details.") + ) + } + /* * AWAIT SSO credentials test */ @@ -1778,6 +1809,43 @@ public class SecureCredentialsManagerTest { ) } + @Test + public fun shouldGetAndFailToRenewExpiredCredentialsWhenAnyUnexpectedErrorOccurs() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } + val expiresAt = Date(CredentialsMock.CURRENT_TIME_MS) + insertTestCredentials(false, true, true, expiresAt, "scope") + Mockito.`when`( + client.renewAuth("refreshToken") + ).thenReturn(request) + //Trigger failure + val runtimeError = + RuntimeException("Something went wrong") + Mockito.`when`(request.execute()).thenThrow(runtimeError) + manager.getCredentials(callback) + verify(callback).onFailure( + exceptionCaptor.capture() + ) + verify(storage, never()) + .store(anyString(), anyLong()) + verify(storage, never()) + .store(anyString(), anyInt()) + verify(storage, never()) + .store(anyString(), anyString()) + verify(storage, never()) + .store(anyString(), anyBoolean()) + verify(storage, never()).remove(anyString()) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat(exception, Is.`is`(CredentialsManagerException.UNKNOWN_ERROR)) + MatcherAssert.assertThat(exception.cause, Is.`is`(runtimeError)) + MatcherAssert.assertThat( + exception.message, + Is.`is`("An unknown error has occurred while fetching the token. Please check the error cause for more details.") + ) + } + /** * Testing that getCredentials execution from multiple threads via multiple instances of SecureCredentialsManager should trigger only one network request. */