From 6513d7d1fff74f59930b5d336efa5b7cf80f17a4 Mon Sep 17 00:00:00 2001 From: Prince Mathew Date: Tue, 18 Nov 2025 16:00:12 +0530 Subject: [PATCH 1/6] refactor : Updated the MRRT key storing logic --- .../storage/BaseCredentialsManager.kt | 35 +++++++++++++++---- .../storage/CredentialsManager.kt | 33 +++++++++++------ .../storage/SecureCredentialsManager.kt | 33 +++++++++-------- 3 files changed, 69 insertions(+), 32 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt index 09c86d037..1805fa56c 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 @@ -3,16 +3,12 @@ package com.auth0.android.authentication.storage import androidx.annotation.VisibleForTesting import com.auth0.android.authentication.AuthenticationAPIClient import com.auth0.android.callback.Callback -import com.auth0.android.request.internal.GsonProvider -import com.auth0.android.request.internal.Jwt import com.auth0.android.result.APICredentials import com.auth0.android.result.Credentials import com.auth0.android.result.SSOCredentials import com.auth0.android.result.UserProfile import com.auth0.android.util.Clock import java.util.* -import kotlin.collections.component1 -import kotlin.collections.component2 /** * Base class meant to abstract common logic across Credentials Manager implementations. @@ -36,7 +32,12 @@ public abstract class BaseCredentialsManager internal constructor( @Throws(CredentialsManagerException::class) public abstract fun saveCredentials(credentials: Credentials) - public abstract fun saveApiCredentials(apiCredentials: APICredentials, audience: String) + public abstract fun saveApiCredentials( + apiCredentials: APICredentials, + audience: String, + scope: String? + ) + public abstract fun getCredentials(callback: Callback) public abstract fun getSsoCredentials( parameters: Map, @@ -158,13 +159,22 @@ public abstract class BaseCredentialsManager internal constructor( * * @param storedScope the stored scope, separated by space characters. * @param requiredScope the required scope, separated by space characters. + * @param ignoreOpenid whether to ignore the openid scope from the storedScope or not while comparing. * @return whether the scope are different or not */ - protected fun hasScopeChanged(storedScope: String?, requiredScope: String?): Boolean { + protected fun hasScopeChanged( + storedScope: String?, + requiredScope: String?, + ignoreOpenid: Boolean = false + ): Boolean { if (requiredScope == null) { return false } - val stored = storedScope.orEmpty().split(" ").toTypedArray() + val storedScopeList = storedScope.orEmpty().split(" ").toMutableList() + + if (ignoreOpenid) storedScopeList.remove("openid") + + val stored = storedScopeList.toTypedArray() Arrays.sort(stored) val required = requiredScope.split(" ").toTypedArray() Arrays.sort(required) @@ -196,4 +206,15 @@ public abstract class BaseCredentialsManager internal constructor( protected fun hasExpired(expiresAt: Long): Boolean { return expiresAt <= currentTimeInMillis } + + /** + * Returns the key for storing the APICredentials in storage. Uses a combination of audience and scope. + * + * @param audience the audience of the credentials. + * @param scope optional scope for the credentials. + */ + protected fun getAPICredentialsKey(audience: String, scope: String?): String { + // Use audience if scope is null else use a combination of audience and scope + return if (scope == null) audience else "$audience::$scope" + } } \ No newline at end of file diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt index b60a6270e..cd313fa29 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,28 +1,24 @@ package com.auth0.android.authentication.storage import android.text.TextUtils -import android.util.Base64 import android.util.Log import androidx.annotation.VisibleForTesting import com.auth0.android.authentication.AuthenticationAPIClient import com.auth0.android.authentication.AuthenticationException -import com.auth0.android.authentication.storage.SecureCredentialsManager.Companion.KEY_CREDENTIALS import com.auth0.android.callback.Callback import com.auth0.android.request.internal.GsonProvider import com.auth0.android.request.internal.Jwt import com.auth0.android.result.APICredentials import com.auth0.android.result.Credentials -import com.auth0.android.result.OptionalCredentials import com.auth0.android.result.SSOCredentials import com.auth0.android.result.UserProfile import com.auth0.android.result.toAPICredentials import com.google.gson.Gson import kotlinx.coroutines.suspendCancellableCoroutine -import java.util.* +import java.util.Date +import java.util.Locale import java.util.concurrent.Executor import java.util.concurrent.Executors -import kotlin.collections.component1 -import kotlin.collections.component2 import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @@ -85,10 +81,16 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting * Stores the given [APICredentials] in the storage for the given audience. * @param apiCredentials the API Credentials to be stored * @param audience the audience for which the credentials are stored + * @param scope the scope for which the credentials are stored */ - override fun saveApiCredentials(apiCredentials: APICredentials, audience: String) { + override fun saveApiCredentials( + apiCredentials: APICredentials, + audience: String, + scope: String? + ) { + val key = getAPICredentialsKey(audience, scope) gson.toJson(apiCredentials).let { - storage.store(audience, it) + storage.store(key, it) } } @@ -594,14 +596,23 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting headers: Map, callback: Callback ) { + serialExecutor.execute { //Check if existing api credentials are present and valid - val apiCredentialsJson = storage.retrieveString(audience) + val key = getAPICredentialsKey(audience, scope) + val apiCredentialsJson = storage.retrieveString(key) apiCredentialsJson?.let { val apiCredentials = gson.fromJson(it, APICredentials::class.java) val willTokenExpire = willExpire(apiCredentials.expiresAt.time, minTtl.toLong()) - val scopeChanged = hasScopeChanged(apiCredentials.scope, scope) + + val scopeChanged = hasScopeChanged( + apiCredentials.scope, + scope, + ignoreOpenid = scope?.contains("openid") == false + ) + val hasExpired = hasExpired(apiCredentials.expiresAt.time) + if (!hasExpired && !willTokenExpire && !scopeChanged) { callback.onSuccess(apiCredentials) return@execute @@ -645,7 +656,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting val newApiCredentials = newCredentials.toAPICredentials() storage.store(KEY_REFRESH_TOKEN, updatedRefreshToken) storage.store(KEY_ID_TOKEN, newCredentials.idToken) - saveApiCredentials(newApiCredentials, audience) + saveApiCredentials(newApiCredentials, audience, scope) callback.onSuccess(newApiCredentials) } catch (error: AuthenticationException) { val exception = when { 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 22c426ec8..b206cd4d2 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 @@ -11,7 +11,6 @@ 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.request.internal.Jwt import com.auth0.android.result.APICredentials import com.auth0.android.result.Credentials import com.auth0.android.result.OptionalCredentials @@ -19,16 +18,12 @@ import com.auth0.android.result.SSOCredentials import com.auth0.android.result.UserProfile import com.auth0.android.result.toAPICredentials import com.google.gson.Gson -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import java.lang.ref.WeakReference -import java.util.* +import java.util.Date +import java.util.Locale import java.util.concurrent.Executor import java.util.concurrent.atomic.AtomicLong -import kotlin.collections.component1 -import kotlin.collections.component2 import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @@ -143,13 +138,19 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT * Stores the given [APICredentials] in the storage for the given audience. * @param apiCredentials the API Credentials to be stored * @param audience the audience for which the credentials are stored + * @param scope the scope for which the credentials are stored */ - override fun saveApiCredentials(apiCredentials: APICredentials, audience: String) { + override fun saveApiCredentials( + apiCredentials: APICredentials, + audience: String, + scope: String? + ) { + val key = getAPICredentialsKey(audience, scope) val json = gson.toJson(apiCredentials) try { val encrypted = crypto.encrypt(json.toByteArray()) val encryptedEncoded = Base64.encodeToString(encrypted, Base64.DEFAULT) - storage.store(audience, encryptedEncoded) + storage.store(key, encryptedEncoded) } catch (e: IncompatibleDeviceException) { throw CredentialsManagerException( CredentialsManagerException.Code.INCOMPATIBLE_DEVICE, @@ -270,7 +271,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT if (credentials == null) { return null } - return credentials.user + return credentials.user } /** @@ -908,7 +909,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT callback: Callback ) { serialExecutor.execute { - val encryptedEncodedJson = storage.retrieveString(audience) + val encryptedEncodedJson = storage.retrieveString(getAPICredentialsKey(audience, scope)) //Check if existing api credentials are present and valid encryptedEncodedJson?.let { encryptedEncoded -> val encrypted = Base64.decode(encryptedEncoded, Base64.DEFAULT) @@ -938,7 +939,10 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT val expiresAt = apiCredentials.expiresAt.time val willAccessTokenExpire = willExpire(expiresAt, minTtl.toLong()) - val scopeChanged = hasScopeChanged(apiCredentials.scope, scope) + val scopeChanged = hasScopeChanged( + apiCredentials.scope, scope, + ignoreOpenid = scope?.contains("openid") == false + ) val hasExpired = hasExpired(apiCredentials.expiresAt.time) if (!hasExpired && !willAccessTokenExpire && !scopeChanged) { callback.onSuccess(apiCredentials) @@ -993,7 +997,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT idToken = newCredentials.idToken ) ) - saveApiCredentials(newApiCredentials, audience) + saveApiCredentials(newApiCredentials, audience, scope) callback.onSuccess(newApiCredentials) } catch (error: AuthenticationException) { @@ -1138,7 +1142,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT internal fun isBiometricSessionValid(): Boolean { val lastAuth = lastBiometricAuthTime.get() if (lastAuth == NO_SESSION) return false // No session exists - + return when (val policy = biometricPolicy) { is BiometricPolicy.Session, is BiometricPolicy.AppLifecycle -> { @@ -1149,6 +1153,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT } * 1000L System.currentTimeMillis() - lastAuth < timeoutMillis } + is BiometricPolicy.Always -> false } } From c0b60958452466b7e79c6ac5da6ee6c0eecf9c80 Mon Sep 17 00:00:00 2001 From: Prince Mathew Date: Tue, 18 Nov 2025 16:00:12 +0530 Subject: [PATCH 2/6] refactor : Updated the MRRT key storing logic --- .../storage/BaseCredentialsManager.kt | 35 +++++++++++++++---- .../storage/CredentialsManager.kt | 25 ++++++++++--- .../storage/SecureCredentialsManager.kt | 20 +++++++---- 3 files changed, 62 insertions(+), 18 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt index 09c86d037..1805fa56c 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 @@ -3,16 +3,12 @@ package com.auth0.android.authentication.storage import androidx.annotation.VisibleForTesting import com.auth0.android.authentication.AuthenticationAPIClient import com.auth0.android.callback.Callback -import com.auth0.android.request.internal.GsonProvider -import com.auth0.android.request.internal.Jwt import com.auth0.android.result.APICredentials import com.auth0.android.result.Credentials import com.auth0.android.result.SSOCredentials import com.auth0.android.result.UserProfile import com.auth0.android.util.Clock import java.util.* -import kotlin.collections.component1 -import kotlin.collections.component2 /** * Base class meant to abstract common logic across Credentials Manager implementations. @@ -36,7 +32,12 @@ public abstract class BaseCredentialsManager internal constructor( @Throws(CredentialsManagerException::class) public abstract fun saveCredentials(credentials: Credentials) - public abstract fun saveApiCredentials(apiCredentials: APICredentials, audience: String) + public abstract fun saveApiCredentials( + apiCredentials: APICredentials, + audience: String, + scope: String? + ) + public abstract fun getCredentials(callback: Callback) public abstract fun getSsoCredentials( parameters: Map, @@ -158,13 +159,22 @@ public abstract class BaseCredentialsManager internal constructor( * * @param storedScope the stored scope, separated by space characters. * @param requiredScope the required scope, separated by space characters. + * @param ignoreOpenid whether to ignore the openid scope from the storedScope or not while comparing. * @return whether the scope are different or not */ - protected fun hasScopeChanged(storedScope: String?, requiredScope: String?): Boolean { + protected fun hasScopeChanged( + storedScope: String?, + requiredScope: String?, + ignoreOpenid: Boolean = false + ): Boolean { if (requiredScope == null) { return false } - val stored = storedScope.orEmpty().split(" ").toTypedArray() + val storedScopeList = storedScope.orEmpty().split(" ").toMutableList() + + if (ignoreOpenid) storedScopeList.remove("openid") + + val stored = storedScopeList.toTypedArray() Arrays.sort(stored) val required = requiredScope.split(" ").toTypedArray() Arrays.sort(required) @@ -196,4 +206,15 @@ public abstract class BaseCredentialsManager internal constructor( protected fun hasExpired(expiresAt: Long): Boolean { return expiresAt <= currentTimeInMillis } + + /** + * Returns the key for storing the APICredentials in storage. Uses a combination of audience and scope. + * + * @param audience the audience of the credentials. + * @param scope optional scope for the credentials. + */ + protected fun getAPICredentialsKey(audience: String, scope: String?): String { + // Use audience if scope is null else use a combination of audience and scope + return if (scope == null) audience else "$audience::$scope" + } } \ No newline at end of file diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt index ef5c3806c..cd313fa29 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 @@ -81,10 +81,16 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting * Stores the given [APICredentials] in the storage for the given audience. * @param apiCredentials the API Credentials to be stored * @param audience the audience for which the credentials are stored + * @param scope the scope for which the credentials are stored */ - override fun saveApiCredentials(apiCredentials: APICredentials, audience: String) { + override fun saveApiCredentials( + apiCredentials: APICredentials, + audience: String, + scope: String? + ) { + val key = getAPICredentialsKey(audience, scope) gson.toJson(apiCredentials).let { - storage.store(audience, it) + storage.store(key, it) } } @@ -590,14 +596,23 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting headers: Map, callback: Callback ) { + serialExecutor.execute { //Check if existing api credentials are present and valid - val apiCredentialsJson = storage.retrieveString(audience) + val key = getAPICredentialsKey(audience, scope) + val apiCredentialsJson = storage.retrieveString(key) apiCredentialsJson?.let { val apiCredentials = gson.fromJson(it, APICredentials::class.java) val willTokenExpire = willExpire(apiCredentials.expiresAt.time, minTtl.toLong()) - val scopeChanged = hasScopeChanged(apiCredentials.scope, scope) + + val scopeChanged = hasScopeChanged( + apiCredentials.scope, + scope, + ignoreOpenid = scope?.contains("openid") == false + ) + val hasExpired = hasExpired(apiCredentials.expiresAt.time) + if (!hasExpired && !willTokenExpire && !scopeChanged) { callback.onSuccess(apiCredentials) return@execute @@ -641,7 +656,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting val newApiCredentials = newCredentials.toAPICredentials() storage.store(KEY_REFRESH_TOKEN, updatedRefreshToken) storage.store(KEY_ID_TOKEN, newCredentials.idToken) - saveApiCredentials(newApiCredentials, audience) + saveApiCredentials(newApiCredentials, audience, scope) callback.onSuccess(newApiCredentials) } catch (error: AuthenticationException) { val exception = when { 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 919c5e8f7..c4aa4a830 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 @@ -208,13 +208,19 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT * Stores the given [APICredentials] in the storage for the given audience. * @param apiCredentials the API Credentials to be stored * @param audience the audience for which the credentials are stored + * @param scope the scope for which the credentials are stored */ - override fun saveApiCredentials(apiCredentials: APICredentials, audience: String) { + override fun saveApiCredentials( + apiCredentials: APICredentials, + audience: String, + scope: String? + ) { + val key = getAPICredentialsKey(audience, scope) val json = gson.toJson(apiCredentials) try { val encrypted = crypto.encrypt(json.toByteArray()) val encryptedEncoded = Base64.encodeToString(encrypted, Base64.DEFAULT) - storage.store(audience, encryptedEncoded) + storage.store(key, encryptedEncoded) } catch (e: IncompatibleDeviceException) { throw CredentialsManagerException( CredentialsManagerException.Code.INCOMPATIBLE_DEVICE, @@ -973,7 +979,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT callback: Callback ) { serialExecutor.execute { - val encryptedEncodedJson = storage.retrieveString(audience) + val encryptedEncodedJson = storage.retrieveString(getAPICredentialsKey(audience, scope)) //Check if existing api credentials are present and valid encryptedEncodedJson?.let { encryptedEncoded -> val encrypted = Base64.decode(encryptedEncoded, Base64.DEFAULT) @@ -1003,7 +1009,10 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT val expiresAt = apiCredentials.expiresAt.time val willAccessTokenExpire = willExpire(expiresAt, minTtl.toLong()) - val scopeChanged = hasScopeChanged(apiCredentials.scope, scope) + val scopeChanged = hasScopeChanged( + apiCredentials.scope, scope, + ignoreOpenid = scope?.contains("openid") == false + ) val hasExpired = hasExpired(apiCredentials.expiresAt.time) if (!hasExpired && !willAccessTokenExpire && !scopeChanged) { callback.onSuccess(apiCredentials) @@ -1058,7 +1067,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT idToken = newCredentials.idToken ) ) - saveApiCredentials(newApiCredentials, audience) + saveApiCredentials(newApiCredentials, audience, scope) callback.onSuccess(newApiCredentials) } catch (error: AuthenticationException) { @@ -1203,7 +1212,6 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT internal fun isBiometricSessionValid(): Boolean { val lastAuth = lastBiometricAuthTime.get() if (lastAuth == NO_SESSION) return false // No session exists - val policy = localAuthenticationOptions?.policy ?: BiometricPolicy.Always return when (policy) { is BiometricPolicy.Session, From cec383f8380b99ede85c19ad61d4e97a78622540 Mon Sep 17 00:00:00 2001 From: Prince Mathew Date: Thu, 20 Nov 2025 10:16:58 +0530 Subject: [PATCH 3/6] Change in the clearAPICredentials method signature --- .../authentication/storage/BaseCredentialsManager.kt | 2 +- .../authentication/storage/CredentialsManager.kt | 5 +++-- .../authentication/storage/SecureCredentialsManager.kt | 10 +++++----- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt index 1805fa56c..73c3628b7 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 @@ -146,7 +146,7 @@ public abstract class BaseCredentialsManager internal constructor( public abstract val userProfile: UserProfile? public abstract fun clearCredentials() - public abstract fun clearApiCredentials(audience: String) + public abstract fun clearApiCredentials(audience: String, scope: String? = null) 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 cd313fa29..0954048d3 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 @@ -718,8 +718,9 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting /** * Removes the credentials for the given audience from the storage if present. */ - override fun clearApiCredentials(audience: String) { - storage.remove(audience) + override fun clearApiCredentials(audience: String, scope: String?) { + val key = getAPICredentialsKey(audience, scope) + storage.remove(key) Log.d(TAG, "API Credentials for $audience were just removed from the storage") } diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt index e979a560e..622cb5043 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 @@ -232,7 +232,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT * to use on the next call. We clear any existing credentials so #hasValidCredentials returns * a true value. Retrying this operation will succeed. */ - clearApiCredentials(audience) + clearApiCredentials(audience, scope) throw CredentialsManagerException( CredentialsManagerException.Code.CRYPTO_EXCEPTION, e @@ -780,8 +780,9 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT /** * Removes the credentials for the given audience from the storage if present. */ - override fun clearApiCredentials(audience: String) { - storage.remove(audience) + override fun clearApiCredentials(audience: String, scope: String?) { + val key = getAPICredentialsKey(audience, scope) + storage.remove(key) Log.d(TAG, "API Credentials for $audience were just removed from the storage") } @@ -994,8 +995,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT ) return@execute } catch (e: CryptoException) { - //If keys were invalidated, existing credentials will not be recoverable. - clearApiCredentials(audience) + clearApiCredentials(audience, scope) callback.onFailure( CredentialsManagerException( CredentialsManagerException.Code.CRYPTO_EXCEPTION, From 7f8391e26f38aa3f559479b218786a6ad71de1c2 Mon Sep 17 00:00:00 2001 From: Prince Mathew Date: Sat, 29 Nov 2025 21:34:38 +0530 Subject: [PATCH 4/6] updated the key storing condition --- .../authentication/storage/BaseCredentialsManager.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt index d763060de..0d191110a 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 @@ -35,7 +35,7 @@ public abstract class BaseCredentialsManager internal constructor( public abstract fun saveApiCredentials( apiCredentials: APICredentials, audience: String, - scope: String? + scope: String? = null ) public abstract fun getCredentials(callback: Callback) @@ -170,7 +170,8 @@ public abstract class BaseCredentialsManager internal constructor( if (requiredScope == null) { return false } - val storedScopes = storedScope.orEmpty().split(" ").filter { it.isNotEmpty() }.toMutableSet() + val storedScopes = + storedScope.orEmpty().split(" ").filter { it.isNotEmpty() }.toMutableSet() if (ignoreOpenid) { storedScopes.remove("openid") } @@ -212,6 +213,6 @@ public abstract class BaseCredentialsManager internal constructor( */ protected fun getAPICredentialsKey(audience: String, scope: String?): String { // Use audience if scope is null else use a combination of audience and scope - return if (scope == null) audience else "$audience::$scope" + return if (scope == null) audience else "$audience::${scope.replace(" ","::")}" } } From ed6830a322aa85c465252a7ef146153889978e2e Mon Sep 17 00:00:00 2001 From: Prince Mathew Date: Sat, 29 Nov 2025 22:17:29 +0530 Subject: [PATCH 5/6] Test cases updated --- .../storage/CredentialsManagerTest.kt | 276 ++++++++++++++---- .../storage/SecureCredentialsManagerTest.kt | 276 +++++++++++++++++- .../android/result/ApiCredentialsMock.kt | 2 +- 3 files changed, 496 insertions(+), 58 deletions(-) diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt index c4397f8c9..cf3924763 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 @@ -432,7 +432,7 @@ public class CredentialsManagerTest { "token", "type", Date(accessTokenExpiry), "scope" ) - Mockito.`when`(storage.retrieveString("audience")).thenReturn(gson.toJson(apiCredentials)) + Mockito.`when`(storage.retrieveString("audience::scope")).thenReturn(gson.toJson(apiCredentials)) manager.getApiCredentials("audience", "scope", callback = apiCredentialsCallback) verify(apiCredentialsCallback).onSuccess(apiCredentialsCaptor.capture()) val retrievedCredentials = apiCredentialsCaptor.firstValue @@ -457,7 +457,7 @@ public class CredentialsManagerTest { @Test public fun shouldRenewApiCredentialsIfThereIsNoExistingApiCredentials() { verifyNoMoreInteractions(client) - Mockito.`when`(storage.retrieveString("audience")).thenReturn(null) + Mockito.`when`(storage.retrieveString("audience::newScope")).thenReturn(null) Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken") Mockito.`when`( client.renewAuth("refreshToken", "audience", "newScope") @@ -481,7 +481,7 @@ public class CredentialsManagerTest { verify(storage).store("com.auth0.id_token", renewedCredentials.idToken) // RefreshToken should not be replaced verify(storage).store("com.auth0.refresh_token", "refreshToken") - verify(storage).store("audience", gson.toJson(renewedCredentials.toAPICredentials())) + verify(storage).store("audience::newScope", gson.toJson(renewedCredentials.toAPICredentials())) // Verify the returned credentials are the latest val newAPiCredentials = apiCredentialsCaptor.firstValue MatcherAssert.assertThat(newAPiCredentials, Is.`is`(Matchers.notNullValue())) @@ -499,7 +499,7 @@ public class CredentialsManagerTest { "token", "type", Date(accessTokenExpiry), "scope" ) - Mockito.`when`(storage.retrieveString("audience")).thenReturn(gson.toJson(apiCredentials)) + Mockito.`when`(storage.retrieveString("audience::scope")).thenReturn(gson.toJson(apiCredentials)) Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken") Mockito.`when`( client.renewAuth("refreshToken", "audience", "scope") @@ -523,7 +523,7 @@ public class CredentialsManagerTest { verify(storage).store("com.auth0.id_token", renewedCredentials.idToken) // RefreshToken should not be replaced verify(storage).store("com.auth0.refresh_token", "refreshToken") - verify(storage).store("audience", gson.toJson(renewedCredentials.toAPICredentials())) + verify(storage).store("audience::scope", gson.toJson(renewedCredentials.toAPICredentials())) // Verify the returned credentials are the latest val newAPiCredentials = apiCredentialsCaptor.firstValue MatcherAssert.assertThat(newAPiCredentials, Is.`is`(Matchers.notNullValue())) @@ -541,7 +541,7 @@ public class CredentialsManagerTest { "token", "type", Date(accessTokenExpiry), "scope" ) - Mockito.`when`(storage.retrieveString("audience")).thenReturn(gson.toJson(apiCredentials)) + Mockito.`when`(storage.retrieveString("audience::scope")).thenReturn(gson.toJson(apiCredentials)) Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken") Mockito.`when`( client.renewAuth("refreshToken", "audience", "scope") @@ -565,7 +565,7 @@ public class CredentialsManagerTest { verify(storage).store("com.auth0.id_token", renewedCredentials.idToken) // RefreshToken should not be replaced verify(storage).store("com.auth0.refresh_token", "refreshToken") - verify(storage).store("audience", gson.toJson(renewedCredentials.toAPICredentials())) + verify(storage).store("audience::scope", gson.toJson(renewedCredentials.toAPICredentials())) // Verify the returned credentials are the latest val newAPiCredentials = apiCredentialsCaptor.firstValue MatcherAssert.assertThat(newAPiCredentials, Is.`is`(Matchers.notNullValue())) @@ -575,55 +575,13 @@ public class CredentialsManagerTest { MatcherAssert.assertThat(newAPiCredentials.scope, Is.`is`("scope")) } - @Test - public fun shouldRenewApiCredentialsIfSavedScopeIsDifferentFromRequiredScope() { - verifyNoMoreInteractions(client) - val accessTokenExpiry = CredentialsMock.ONE_HOUR_AHEAD_MS - val apiCredentials = ApiCredentialsMock.create( - "token", "type", - Date(accessTokenExpiry), "scope" - ) - Mockito.`when`(storage.retrieveString("audience")).thenReturn(gson.toJson(apiCredentials)) - Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken") - Mockito.`when`( - client.renewAuth("refreshToken", "audience", "newScope") - ).thenReturn(request) - val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS + ONE_HOUR_SECONDS * 1000) - val jwtMock = mock() - Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) - Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) - - // Trigger success - val newRefresh: String? = null - val renewedCredentials = - Credentials("newId", "newAccess", "newType", newRefresh, newDate, "newScope") - Mockito.`when`(request.execute()).thenReturn(renewedCredentials) - manager.getApiCredentials("audience", "newScope", callback = apiCredentialsCallback) - verify(apiCredentialsCallback).onSuccess( - apiCredentialsCaptor.capture() - ) - - // Verify the credentials are property stored - verify(storage).store("com.auth0.id_token", renewedCredentials.idToken) - // RefreshToken should not be replaced - verify(storage).store("com.auth0.refresh_token", "refreshToken") - verify(storage).store("audience", gson.toJson(renewedCredentials.toAPICredentials())) - // Verify the returned credentials are the latest - val newAPiCredentials = apiCredentialsCaptor.firstValue - MatcherAssert.assertThat(newAPiCredentials, Is.`is`(Matchers.notNullValue())) - MatcherAssert.assertThat(newAPiCredentials.accessToken, Is.`is`("newAccess")) - MatcherAssert.assertThat(newAPiCredentials.type, Is.`is`("newType")) - MatcherAssert.assertThat(newAPiCredentials.expiresAt, Is.`is`(newDate)) - MatcherAssert.assertThat(newAPiCredentials.scope, Is.`is`("newScope")) - } - @Test public fun shouldReplaceTheExistingRefreshTokenIfaNewOneIsObtainedInApiCredentials() { verifyNoMoreInteractions(client) Mockito.`when`(storage.retrieveString("audience")).thenReturn(null) Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken") Mockito.`when`( - client.renewAuth("refreshToken", "audience", "newScope") + client.renewAuth("refreshToken", "audience","newScope") ).thenReturn(request) val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS + ONE_HOUR_SECONDS * 1000) val jwtMock = mock() @@ -643,7 +601,7 @@ public class CredentialsManagerTest { verify(storage).store("com.auth0.id_token", renewedCredentials.idToken) // RefreshToken should be replaced verify(storage).store("com.auth0.refresh_token", "newRefreshToken") - verify(storage).store("audience", gson.toJson(renewedCredentials.toAPICredentials())) + verify(storage).store("audience::newScope", gson.toJson(renewedCredentials.toAPICredentials())) // Verify the returned credentials are the latest val newAPiCredentials = apiCredentialsCaptor.firstValue MatcherAssert.assertThat(newAPiCredentials, Is.`is`(Matchers.notNullValue())) @@ -692,7 +650,7 @@ public class CredentialsManagerTest { "token", "type", Date(accessTokenExpiry), "scope" ) - Mockito.`when`(storage.retrieveString("audience")).thenReturn(gson.toJson(apiCredentials)) + Mockito.`when`(storage.retrieveString("audience::scope")).thenReturn(gson.toJson(apiCredentials)) val retrievedCredentials = manager.awaitApiCredentials("audience", "scope") MatcherAssert.assertThat(retrievedCredentials, Is.`is`(Matchers.notNullValue())) Assert.assertEquals(retrievedCredentials.accessToken, apiCredentials.accessToken) @@ -1535,6 +1493,220 @@ public class CredentialsManagerTest { verifyNoMoreInteractions(storage) } + @Test + public fun shouldSaveApiCredentialsWithScopeAsKey() { + val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS + val apiCredentials = APICredentials( + accessToken = "apiAccessToken", + type = "type", + expiresAt = Date(expirationTime), + scope = "read:data write:data" + ) + + manager.saveApiCredentials(apiCredentials, "audience", "read:data write:data") + + verify(storage).store( + eq("audience::read:data::write:data"), + eq(gson.toJson(apiCredentials)) + ) + } + + @Test + public fun shouldSaveApiCredentialsWithoutScopeUsingOnlyAudience() { + val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS + val apiCredentials = APICredentials( + accessToken = "apiAccessToken", + type = "type", + expiresAt = Date(expirationTime), + scope = "read:data" + ) + + manager.saveApiCredentials(apiCredentials, "audience", null) + + verify(storage).store(eq("audience"), eq(gson.toJson(apiCredentials))) + } + + @Test + public fun shouldSaveApiCredentialsWithDifferentScopesUnderDifferentKeys() { + val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS + val apiCredentials1 = APICredentials( + accessToken = "apiAccessToken1", + type = "type", + expiresAt = Date(expirationTime), + scope = "read:data" + ) + val apiCredentials2 = APICredentials( + accessToken = "apiAccessToken2", + type = "type", + expiresAt = Date(expirationTime), + scope = "write:data" + ) + + manager.saveApiCredentials(apiCredentials1, "audience", "read:data") + manager.saveApiCredentials(apiCredentials2, "audience", "write:data") + + verify(storage).store(eq("audience::read:data"), eq(gson.toJson(apiCredentials1))) + verify(storage).store(eq("audience::write:data"), eq(gson.toJson(apiCredentials2))) + } + + @Test + public fun shouldClearApiCredentialsWithScope() { + manager.clearApiCredentials("audience", "read:data write:data") + verify(storage).remove("audience::read:data::write:data") + } + + @Test + public fun shouldClearApiCredentialsWithoutScopeUsingAudienceOnly() { + manager.clearApiCredentials("audience", null) + verify(storage).remove("audience") + } + + @Test + public fun shouldClearApiCredentialsWithDefaultNullScope() { + manager.clearApiCredentials("audience") + verify(storage).remove("audience") + } + + @Test + public fun shouldGetApiCredentialsWithSpecificScope() { + verifyNoMoreInteractions(client) + val accessTokenExpiry = CredentialsMock.ONE_HOUR_AHEAD_MS + val apiCredentials = ApiCredentialsMock.create( + accessToken = "apiToken", + expiresAt = Date(accessTokenExpiry), + scope = "read:data" + ) + Mockito.`when`(storage.retrieveString("audience::read:data")) + .thenReturn(gson.toJson(apiCredentials)) + + manager.getApiCredentials("audience", "read:data", callback = apiCredentialsCallback) + + verify(apiCredentialsCallback).onSuccess(apiCredentialsCaptor.capture()) + val retrievedCredentials = apiCredentialsCaptor.firstValue + MatcherAssert.assertThat(retrievedCredentials.accessToken, Is.`is`("apiToken")) + MatcherAssert.assertThat(retrievedCredentials.scope, Is.`is`("read:data")) + } + + @Test + public fun shouldGetApiCredentialsWithoutScopeFromAudienceKey() { + verifyNoMoreInteractions(client) + val accessTokenExpiry = CredentialsMock.ONE_HOUR_AHEAD_MS + val apiCredentials = ApiCredentialsMock.create( + accessToken = "apiToken", + expiresAt = Date(accessTokenExpiry), + scope = "openid" + ) + Mockito.`when`(storage.retrieveString("audience")) + .thenReturn(gson.toJson(apiCredentials)) + + manager.getApiCredentials("audience", null, callback = apiCredentialsCallback) + + verify(apiCredentialsCallback).onSuccess(apiCredentialsCaptor.capture()) + val retrievedCredentials = apiCredentialsCaptor.firstValue + MatcherAssert.assertThat(retrievedCredentials.accessToken, Is.`is`("apiToken")) + } + + @Test + public fun shouldRenewApiCredentialsWhenRequestingScopeButStoredUnderDifferentScopeKey() { + verifyNoMoreInteractions(client) + val accessTokenExpiry = CredentialsMock.ONE_HOUR_AHEAD_MS + Mockito.`when`(storage.retrieveString("audience::write:data")) + .thenReturn(null) + Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")) + .thenReturn("refreshToken") + Mockito.`when`(client.renewAuth("refreshToken", "audience", "write:data")) + .thenReturn(request) + + val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS + ONE_HOUR_SECONDS * 1000) + val jwtMock = mock() + Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) + Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) + + val renewedCredentials = Credentials( + "newId", "newAccess", "newType", null, newDate, "write:data" + ) + Mockito.`when`(request.execute()).thenReturn(renewedCredentials) + + manager.getApiCredentials("audience", "write:data", callback = apiCredentialsCallback) + + verify(apiCredentialsCallback).onSuccess(apiCredentialsCaptor.capture()) + val newApiCredentials = apiCredentialsCaptor.firstValue + MatcherAssert.assertThat(newApiCredentials.scope, Is.`is`("write:data")) + MatcherAssert.assertThat(newApiCredentials.accessToken, Is.`is`("newAccess")) + verify(storage).store(eq("audience::write:data"), any()) + } + + @Test + public fun shouldStoreApiCredentialsUnderCorrectKeyWhenRenewing() { + verifyNoMoreInteractions(client) + Mockito.`when`(storage.retrieveString("audience::custom:scope")) + .thenReturn(null) + Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")) + .thenReturn("refreshToken") + Mockito.`when`(client.renewAuth("refreshToken", "audience", "custom:scope")) + .thenReturn(request) + + val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS + ONE_HOUR_SECONDS * 1000) + val jwtMock = mock() + Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) + Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) + + val renewedCredentials = Credentials( + "newId", "newAccess", "newType", null, newDate, "custom:scope" + ) + Mockito.`when`(request.execute()).thenReturn(renewedCredentials) + + manager.getApiCredentials("audience", "custom:scope", callback = apiCredentialsCallback) + + verify(storage).store(eq("audience::custom:scope"), any()) + } + + @Test + @ExperimentalCoroutinesApi + public fun shouldAwaitApiCredentialsWithSpecificScope(): Unit = runTest { + verifyNoMoreInteractions(client) + val accessTokenExpiry = CredentialsMock.ONE_HOUR_AHEAD_MS + val apiCredentials = ApiCredentialsMock.create( + accessToken = "apiToken", + expiresAt = Date(accessTokenExpiry), + scope = "read:data" + ) + Mockito.`when`(storage.retrieveString("audience::read:data")) + .thenReturn(gson.toJson(apiCredentials)) + + val result = manager.awaitApiCredentials("audience", "read:data") + + MatcherAssert.assertThat(result.accessToken, Is.`is`("apiToken")) + MatcherAssert.assertThat(result.scope, Is.`is`("read:data")) + } + + @Test + @ExperimentalCoroutinesApi + public fun shouldAwaitAndRenewApiCredentialsWithScope(): Unit = runTest { + verifyNoMoreInteractions(client) + Mockito.`when`(storage.retrieveString("audience::write:data")) + .thenReturn(null) + Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")) + .thenReturn("refreshToken") + Mockito.`when`(client.renewAuth("refreshToken", "audience", "write:data")) + .thenReturn(request) + + val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS + ONE_HOUR_SECONDS * 1000) + val jwtMock = mock() + Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) + Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) + + val renewedCredentials = Credentials( + "newId", "newAccess", "newType", null, newDate, "write:data" + ) + Mockito.`when`(request.execute()).thenReturn(renewedCredentials) + + val result = manager.awaitApiCredentials("audience", "write:data") + + MatcherAssert.assertThat(result.scope, Is.`is`("write:data")) + MatcherAssert.assertThat(result.accessToken, Is.`is`("newAccess")) + } + @Test public fun shouldHaveCredentialsWhenTokenHasNotExpired() { val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS 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 b91ebe55a..893b56fbd 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 @@ -19,12 +19,12 @@ import com.auth0.android.result.Credentials import com.auth0.android.result.CredentialsMock import com.auth0.android.result.SSOCredentials import com.auth0.android.result.SSOCredentialsMock +import com.auth0.android.result.toAPICredentials import com.auth0.android.util.Clock import com.google.gson.Gson import com.nhaarman.mockitokotlin2.KArgumentCaptor import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.doNothing import com.nhaarman.mockitokotlin2.eq import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.never @@ -173,7 +173,7 @@ public class SecureCredentialsManagerTest { ) MatcherAssert.assertThat(manager, Is.`is`(Matchers.notNullValue())) } - + /* * SAVE SSO credentials test */ @@ -2001,6 +2001,272 @@ public class SecureCredentialsManagerTest { verifyNoMoreInteractions(storage) } + @Test + public fun shouldSaveEncryptedApiCredentialsWithScopeAsKey() { + val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS + val apiCredentials = APICredentials( + accessToken = "apiAccessToken", + type = "type", + expiresAt = Date(expirationTime), + scope = "read:data write:data" + ) + val json = gson.toJson(apiCredentials) + Mockito.`when`(crypto.encrypt(json.toByteArray())).thenReturn(json.toByteArray()) + + manager.saveApiCredentials(apiCredentials, "audience", "read:data write:data") + + verify(crypto).encrypt(json.toByteArray()) + verify(storage).store( + eq("audience::read:data::write:data"), + anyString() + ) + } + + @Test + public fun shouldSaveEncryptedApiCredentialsWithoutScopeUsingOnlyAudience() { + val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS + val apiCredentials = APICredentials( + accessToken = "apiAccessToken", + type = "type", + expiresAt = Date(expirationTime), + scope = "read:data" + ) + val json = gson.toJson(apiCredentials) + Mockito.`when`(crypto.encrypt(json.toByteArray())).thenReturn(json.toByteArray()) + + manager.saveApiCredentials(apiCredentials, "audience", null) + + verify(crypto).encrypt(json.toByteArray()) + verify(storage).store(eq("audience"), anyString()) + } + + @Test + public fun shouldSaveEncryptedApiCredentialsWithDifferentScopesUnderDifferentKeys() { + val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS + val apiCredentials1 = APICredentials( + accessToken = "apiAccessToken1", + type = "type", + expiresAt = Date(expirationTime), + scope = "read:data" + ) + val apiCredentials2 = APICredentials( + accessToken = "apiAccessToken2", + type = "type", + expiresAt = Date(expirationTime), + scope = "write:data" + ) + val json1 = gson.toJson(apiCredentials1) + val json2 = gson.toJson(apiCredentials2) + Mockito.`when`(crypto.encrypt(json1.toByteArray())).thenReturn(json1.toByteArray()) + Mockito.`when`(crypto.encrypt(json2.toByteArray())).thenReturn(json2.toByteArray()) + + manager.saveApiCredentials(apiCredentials1, "audience", "read:data") + manager.saveApiCredentials(apiCredentials2, "audience", "write:data") + + verify(storage).store(eq("audience::read:data"), anyString()) + verify(storage).store(eq("audience::write:data"), anyString()) + } + + @Test + public fun shouldThrowIncompatibleDeviceExceptionOnSaveApiCredentials() { + val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS + val apiCredentials = APICredentials( + accessToken = "apiAccessToken", + type = "type", + expiresAt = Date(expirationTime), + scope = "read:data" + ) + val json = gson.toJson(apiCredentials) + Mockito.`when`(crypto.encrypt(json.toByteArray())) + .thenThrow(IncompatibleDeviceException(RuntimeException())) + + val exception = assertThrows(CredentialsManagerException::class.java) { + manager.saveApiCredentials(apiCredentials, "audience", "read:data") + } + + MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat(exception.isDeviceIncompatible, Is.`is`(true)) + MatcherAssert.assertThat( + exception.message, + Is.`is`("This device is not compatible with the SecureCredentialsManager class.") + ) + } + + @Test + public fun shouldClearAndThrowCryptoExceptionOnSaveApiCredentials() { + val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS + val apiCredentials = APICredentials( + accessToken = "apiAccessToken", + type = "type", + expiresAt = Date(expirationTime), + scope = "read:data" + ) + val json = gson.toJson(apiCredentials) + Mockito.`when`(crypto.encrypt(json.toByteArray())) + .thenThrow(CryptoException("Encryption failed", RuntimeException())) + + val exception = assertThrows(CredentialsManagerException::class.java) { + manager.saveApiCredentials(apiCredentials, "audience", "read:data") + } + + verify(storage).remove("audience::read:data") + MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) + 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. Any previously stored content is now lost. Please try saving the credentials again.") + ) + } + + @Test + public fun shouldClearApiCredentialsWithScopeInSecureManager() { + manager.clearApiCredentials("audience", "read:data write:data") + verify(storage).remove("audience::read:data::write:data") + } + + @Test + public fun shouldClearApiCredentialsWithoutScopeUsingAudienceOnlyInSecureManager() { + manager.clearApiCredentials("audience", null) + verify(storage).remove("audience") + } + + @Test + public fun shouldClearApiCredentialsWithDefaultNullScopeInSecureManager() { + manager.clearApiCredentials("audience") + verify(storage).remove("audience") + } + + @Test + public fun shouldGetEncryptedApiCredentialsWithSpecificScope() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } + verifyNoMoreInteractions(client) + val accessTokenExpiry = CredentialsMock.ONE_HOUR_AHEAD_MS + val apiCredentials = ApiCredentialsMock.create( + accessToken = "apiToken", + expiresAt = Date(accessTokenExpiry), + scope = "read:data" + ) + val json = gson.toJson(apiCredentials) + val encryptedJson = Base64.encodeToString(json.toByteArray(), Base64.DEFAULT) + Mockito.`when`(storage.retrieveString("audience::read:data")) + .thenReturn(encryptedJson) + Mockito.`when`(crypto.decrypt(any())) + .thenReturn(json.toByteArray()) + + manager.getApiCredentials("audience", "read:data", callback = apiCredentialsCallback) + + verify(apiCredentialsCallback).onSuccess(apiCredentialsCaptor.capture()) + val retrievedCredentials = apiCredentialsCaptor.firstValue + MatcherAssert.assertThat(retrievedCredentials.accessToken, Is.`is`("apiToken")) + MatcherAssert.assertThat(retrievedCredentials.scope, Is.`is`("read:data")) + } + + @Test + public fun shouldGetEncryptedApiCredentialsWithoutScopeFromAudienceKey() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } + verifyNoMoreInteractions(client) + val accessTokenExpiry = CredentialsMock.ONE_HOUR_AHEAD_MS + val apiCredentials = ApiCredentialsMock.create( + accessToken = "apiToken", + expiresAt = Date(accessTokenExpiry), + scope = "openid" + ) + val json = gson.toJson(apiCredentials) + val encryptedJson = Base64.encodeToString(json.toByteArray(), Base64.DEFAULT) + Mockito.`when`(storage.retrieveString("audience")) + .thenReturn(encryptedJson) + Mockito.`when`(crypto.decrypt(any())) + .thenReturn(json.toByteArray()) + + manager.getApiCredentials("audience", null, callback = apiCredentialsCallback) + + verify(apiCredentialsCallback).onSuccess(apiCredentialsCaptor.capture()) + val retrievedCredentials = apiCredentialsCaptor.firstValue + MatcherAssert.assertThat(retrievedCredentials.accessToken, Is.`is`("apiToken")) + } + + @Test + @ExperimentalCoroutinesApi + public fun shouldAwaitEncryptedApiCredentialsWithSpecificScope(): Unit = runTest { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } + verifyNoMoreInteractions(client) + val accessTokenExpiry = CredentialsMock.ONE_HOUR_AHEAD_MS + val apiCredentials = ApiCredentialsMock.create( + accessToken = "apiToken", + expiresAt = Date(accessTokenExpiry), + scope = "read:data" + ) + val json = gson.toJson(apiCredentials) + val encryptedJson = Base64.encodeToString(json.toByteArray(), Base64.DEFAULT) + Mockito.`when`(storage.retrieveString("audience::read:data")) + .thenReturn(encryptedJson) + Mockito.`when`(crypto.decrypt(any())) + .thenReturn(json.toByteArray()) + + val result = manager.awaitApiCredentials("audience", "read:data") + + MatcherAssert.assertThat(result.accessToken, Is.`is`("apiToken")) + MatcherAssert.assertThat(result.scope, Is.`is`("read:data")) + } + + @Test + @ExperimentalCoroutinesApi + public fun shouldAwaitAndRenewEncryptedApiCredentialsWithScope(): Unit = runTest { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } + verifyNoMoreInteractions(client) + Mockito.`when`(storage.retrieveString("audience::write:data")) + .thenReturn(null) + + val existingCredentials = CredentialsMock.create( + idToken = "idToken", + accessToken = "accessToken", + type = "type", + refreshToken = "refreshToken", + expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS), + scope = "scope" + ) + val existingJson = gson.toJson(existingCredentials) + val encryptedExisting = Base64.encodeToString(existingJson.toByteArray(), Base64.DEFAULT) + Mockito.`when`(storage.retrieveString("com.auth0.credentials")) + .thenReturn(encryptedExisting) + Mockito.`when`(crypto.decrypt(any())) + .thenReturn(existingJson.toByteArray()) + + Mockito.`when`(client.renewAuth("refreshToken", "audience", "write:data")) + .thenReturn(request) + + val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS + ONE_HOUR_SECONDS * 1000) + val jwtMock = mock() + Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) + Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) + + val renewedCredentials = Credentials( + "newId", "newAccess", "newType", null, newDate, "write:data" + ) + Mockito.`when`(request.execute()).thenReturn(renewedCredentials) + + val updatedExistingTokenJson = gson.toJson(existingCredentials.copy(idToken = "newId")) + val renewedJson = gson.toJson(renewedCredentials.toAPICredentials()) + Mockito.`when`(crypto.encrypt(updatedExistingTokenJson.toByteArray())) + .thenReturn(updatedExistingTokenJson.toByteArray()) + + Mockito.`when`(crypto.encrypt(renewedJson.toByteArray())) + .thenReturn(renewedJson.toByteArray()) + + val result = manager.awaitApiCredentials("audience", "write:data") + + MatcherAssert.assertThat(result.scope, Is.`is`("write:data")) + MatcherAssert.assertThat(result.accessToken, Is.`is`("newAccess")) + } + /* * HAS Credentials tests */ @@ -2115,7 +2381,7 @@ public class SecureCredentialsManagerTest { val encoded = String(Base64.encode(storedJson.toByteArray(), Base64.DEFAULT)) Mockito.`when`(crypto.decrypt(storedJson.toByteArray())) .thenReturn(storedJson.toByteArray()) - Mockito.`when`(storage.retrieveString("audience")).thenReturn(encoded) + Mockito.`when`(storage.retrieveString("audience::scope")).thenReturn(encoded) manager.getApiCredentials("audience", "scope", callback = apiCredentialsCallback) verify(apiCredentialsCallback).onSuccess(apiCredentialsCaptor.capture()) val retrievedCredentials = apiCredentialsCaptor.firstValue @@ -2188,8 +2454,8 @@ public class SecureCredentialsManagerTest { // Verify the credentials are property stored verify(storage).store(eq("com.auth0.credentials"), stringCaptor.capture()) MatcherAssert.assertThat(stringCaptor.firstValue, Is.`is`(Matchers.notNullValue())) - val credentials = gson.fromJson(expectedJson,Credentials::class.java) - Assert.assertEquals("refreshToken",credentials.refreshToken) + val credentials = gson.fromJson(expectedJson, Credentials::class.java) + Assert.assertEquals("refreshToken", credentials.refreshToken) // Verify the returned credentials are the latest val newAPiCredentials = apiCredentialsCaptor.firstValue MatcherAssert.assertThat(newAPiCredentials, Is.`is`(Matchers.notNullValue())) diff --git a/auth0/src/test/java/com/auth0/android/result/ApiCredentialsMock.kt b/auth0/src/test/java/com/auth0/android/result/ApiCredentialsMock.kt index be9e0f72e..097225d8b 100644 --- a/auth0/src/test/java/com/auth0/android/result/ApiCredentialsMock.kt +++ b/auth0/src/test/java/com/auth0/android/result/ApiCredentialsMock.kt @@ -8,7 +8,7 @@ public class ApiCredentialsMock { public fun create( accessToken: String, - type: String, + type: String = "Bearer", expiresAt: Date, scope: String, ): APICredentials { From 628b73c7314ec41e2bc33cc2b6e9a9898c7ed0af Mon Sep 17 00:00:00 2001 From: Prince Mathew Date: Fri, 5 Dec 2025 17:18:23 +0530 Subject: [PATCH 6/6] Sorting the scope before saving them with audience --- .../authentication/storage/BaseCredentialsManager.kt | 6 +++++- .../android/authentication/storage/CredentialsManager.kt | 3 +++ .../authentication/storage/SecureCredentialsManager.kt | 3 +++ 3 files changed, 11 insertions(+), 1 deletion(-) 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 0d191110a..d3ac32d59 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt @@ -1,5 +1,6 @@ package com.auth0.android.authentication.storage +import android.util.Log import androidx.annotation.VisibleForTesting import com.auth0.android.authentication.AuthenticationAPIClient import com.auth0.android.callback.Callback @@ -213,6 +214,9 @@ public abstract class BaseCredentialsManager internal constructor( */ protected fun getAPICredentialsKey(audience: String, scope: String?): String { // Use audience if scope is null else use a combination of audience and scope - return if (scope == null) audience else "$audience::${scope.replace(" ","::")}" + if (scope == null) return audience + val sortedScope = scope.split(" ").sorted().joinToString("::") + return "$audience::${sortedScope}" + } } 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 b46e1d113..2be12fc26 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 @@ -717,6 +717,9 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting /** * Removes the credentials for the given audience from the storage if present. + * @param audience Audience for which the [APICredentials] are stored + * @param scope Optional scope for which the [APICredentials] are stored. If the credentials were initially fetched/stored with scope, + * it is recommended to pass scope also while clearing them. */ override fun clearApiCredentials(audience: String, scope: String?) { val key = getAPICredentialsKey(audience, scope) 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 ef53804c9..70abe7ada 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 @@ -764,6 +764,9 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT /** * Removes the credentials for the given audience from the storage if present. + * @param audience Audience for which the [APICredentials] are stored + * @param scope Optional scope for which the [APICredentials] are stored. If the credentials were initially fetched/stored with scope, + * it is recommended to pass scope also while clearing them. */ override fun clearApiCredentials(audience: String, scope: String?) { val key = getAPICredentialsKey(audience, scope)