From 9395de6c74613ac697cb944391530acfd70fa08f Mon Sep 17 00:00:00 2001 From: Prince Mathew Date: Tue, 3 Dec 2024 12:52:42 +0530 Subject: [PATCH] Added new error types for CredentialManagerException --- .../storage/CredentialsManager.kt | 8 +- .../storage/CredentialsManagerException.kt | 10 ++ .../storage/SecureCredentialsManager.kt | 12 +- .../storage/CredentialsManagerTest.kt | 132 +++++++++++++++++- .../storage/SecureCredentialsManagerTest.kt | 121 +++++++++++++++- sample/src/main/res/values/strings.xml | 2 +- 6 files changed, 275 insertions(+), 10 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt index 9e6af7d9..5d494f5e 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 @@ -325,9 +325,15 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting saveCredentials(credentials) callback.onSuccess(credentials) } catch (error: AuthenticationException) { + val exception = when { + error.isRefreshTokenDeleted || + error.isInvalidRefreshToken -> CredentialsManagerException.Code.RENEW_FAILED + error.isNetworkError -> CredentialsManagerException.Code.NO_NETWORK + else -> CredentialsManagerException.Code.SERVER_ERROR + } callback.onFailure( CredentialsManagerException( - CredentialsManagerException.Code.RENEW_FAILED, + exception, error ) ) 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 f90af7a3..9212546c 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 @@ -43,6 +43,8 @@ public class CredentialsManagerException : BIOMETRIC_ERROR_UNABLE_TO_PROCESS, BIOMETRICS_INVALID_USER, BIOMETRIC_AUTHENTICATION_FAILED, + NO_NETWORK, + SERVER_ERROR } private var code: Code? @@ -135,6 +137,12 @@ public class CredentialsManagerException : public val BIOMETRICS_INVALID_USER: CredentialsManagerException = CredentialsManagerException(Code.BIOMETRICS_INVALID_USER) + //Exceptions thrown when making api calls for access token renewal + public val NO_NETWORK: CredentialsManagerException = + CredentialsManagerException(Code.NO_NETWORK) + public val SERVER_ERROR: CredentialsManagerException = + CredentialsManagerException(Code.SERVER_ERROR) + private fun getMessage(code: Code): String { return when (code) { @@ -177,6 +185,8 @@ public class CredentialsManagerException : 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." + Code.NO_NETWORK -> "Failed to execute the network request." + Code.SERVER_ERROR -> "An error occurred when trying to authenticate with the server." } } } 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 fcbe4aef..37dd2311 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 @@ -7,8 +7,8 @@ import android.util.Log import androidx.annotation.VisibleForTesting 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.Callback import com.auth0.android.request.internal.GsonProvider import com.auth0.android.result.Credentials @@ -585,10 +585,16 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT fresh.expiresAt, fresh.scope ) - } catch (error: Auth0Exception) { + } catch (error: AuthenticationException) { + val exception = when { + error.isRefreshTokenDeleted || + error.isInvalidRefreshToken -> CredentialsManagerException.Code.RENEW_FAILED + error.isNetworkError -> CredentialsManagerException.Code.NO_NETWORK + else -> CredentialsManagerException.Code.SERVER_ERROR + } callback.onFailure( CredentialsManagerException( - CredentialsManagerException.Code.RENEW_FAILED, + exception, error ) ) 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 98c4faff..3169c653 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 @@ -1,5 +1,6 @@ package com.auth0.android.authentication.storage +import com.auth0.android.NetworkErrorException import com.auth0.android.authentication.AuthenticationAPIClient import com.auth0.android.authentication.AuthenticationException import com.auth0.android.callback.Callback @@ -788,7 +789,7 @@ public class CredentialsManagerTest { } @Test - public fun shouldGetAndFailToRenewExpiredCredentials() { + public fun shouldGetAndFailToRenewExpiredCredentialsWhenRefreshTokenExpired() { 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") @@ -802,8 +803,9 @@ public class CredentialsManagerTest { client.renewAuth("refreshToken") ).thenReturn(request) //Trigger failure - val authenticationException = Mockito.mock( - AuthenticationException::class.java + val authenticationException = AuthenticationException( + "invalid_grant", + "Unknown or invalid refresh token." ) Mockito.`when`(request.execute()).thenThrow(authenticationException) manager.getCredentials(callback) @@ -828,6 +830,130 @@ public class CredentialsManagerTest { ) } + @Test + public fun shouldGetAndFailToRenewExpiredCredentialsWhenUserIsDeleted() { + 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 authenticationException = AuthenticationException( + mapOf( + "error" to "invalid_grant", + "error_description" to "The refresh_token was generated for a user who doesn't exist anymore." + ), 403 + ) + Mockito.`when`(request.execute()).thenThrow(authenticationException) + 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.cause, Is.`is`(authenticationException)) + MatcherAssert.assertThat( + exception.message, + Is.`is`("An error occurred while trying to use the Refresh Token to renew the Credentials.") + ) + } + + @Test + public fun shouldGetAndFailToRenewExpiredCredentialsWhenNetworkIsNotAvailable() { + 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 authenticationException = AuthenticationException( + "Failed to execute the network request", NetworkErrorException(mock()) + ) + Mockito.`when`(request.execute()).thenThrow(authenticationException) + 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.cause, Is.`is`(authenticationException)) + MatcherAssert.assertThat( + exception.message, + Is.`is`("Failed to execute the network request.") + ) + } + + @Test + public fun shouldGetAndFailToRenewExpiredCredentialsWhenServerErrorOccurs() { + 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 authenticationException = AuthenticationException("Something went wrong", mock()) + Mockito.`when`(request.execute()).thenThrow(authenticationException) + 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.cause, Is.`is`(authenticationException)) + MatcherAssert.assertThat( + exception.message, + Is.`is`("An error occurred when trying to authenticate with the server.") + ) + } + @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 6a78c50c..54414e29 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 @@ -6,6 +6,7 @@ import android.content.Context import android.util.Base64 import androidx.fragment.app.FragmentActivity import com.auth0.android.Auth0 +import com.auth0.android.NetworkErrorException import com.auth0.android.authentication.AuthenticationAPIClient import com.auth0.android.authentication.AuthenticationException import com.auth0.android.callback.Callback @@ -1259,7 +1260,7 @@ public class SecureCredentialsManagerTest { } @Test - public fun shouldGetAndFailToRenewExpiredCredentials() { + public fun shouldGetAndFailToRenewExpiredCredentialsWhenRefreshTokenExpired() { Mockito.`when`(localAuthenticationManager.authenticate()).then { localAuthenticationManager.resultCallback.onSuccess(true) } @@ -1269,7 +1270,50 @@ public class SecureCredentialsManagerTest { client.renewAuth("refreshToken") ).thenReturn(request) //Trigger failure - val authenticationException = mock() + val authenticationException = AuthenticationException( + "invalid_grant", + "Unknown or invalid refresh token." + ) + Mockito.`when`(request.execute()).thenThrow(authenticationException) + 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.cause, Is.`is`(authenticationException)) + MatcherAssert.assertThat( + exception.message, + Is.`is`("An error occurred while trying to use the Refresh Token to renew the Credentials.") + ) + } + + @Test + public fun shouldGetAndFailToRenewExpiredCredentialsWhenUserIsDeleted() { + 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 authenticationException = AuthenticationException( + mapOf( + "error" to "invalid_grant", + "error_description" to "The refresh_token was generated for a user who doesn't exist anymore." + ), 403 + ) Mockito.`when`(request.execute()).thenThrow(authenticationException) manager.getCredentials(callback) verify(callback).onFailure( @@ -1293,6 +1337,79 @@ public class SecureCredentialsManagerTest { ) } + @Test + public fun shouldGetAndFailToRenewExpiredCredentialsWhenNetworkIsNotAvailable() { + 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 authenticationException = AuthenticationException( + "Failed to execute the network request", NetworkErrorException(mock()) + ) + Mockito.`when`(request.execute()).thenThrow(authenticationException) + 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.cause, Is.`is`(authenticationException)) + MatcherAssert.assertThat( + exception.message, + Is.`is`("Failed to execute the network request.") + ) + } + + @Test + public fun shouldGetAndFailToRenewExpiredCredentialsWhenServerErrorOccurs() { + 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 authenticationException = + AuthenticationException("Something went wrong", mock()) + Mockito.`when`(request.execute()).thenThrow(authenticationException) + 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.cause, Is.`is`(authenticationException)) + MatcherAssert.assertThat( + exception.message, + Is.`is`("An error occurred when trying to authenticate with the server.") + ) + } + /** * Testing that getCredentials execution from multiple threads via multiple instances of SecureCredentialsManager should trigger only one network request. */ diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml index b87a506a..91629328 100644 --- a/sample/src/main/res/values/strings.xml +++ b/sample/src/main/res/values/strings.xml @@ -1,6 +1,6 @@ Auth0 SDK Sample - mathewp.acmetest.org + p-mathew.us.auth0.com gkba7X6OJM2b0cdlUlTCqXD7AwT3FYVV demo \ No newline at end of file