Skip to content

Commit 5785a50

Browse files
authored
refactor: Updating MRRT token store logic (#884)
1 parent 7df5af9 commit 5785a50

File tree

6 files changed

+585
-101
lines changed

6 files changed

+585
-101
lines changed

auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
package com.auth0.android.authentication.storage
22

3+
import android.util.Log
34
import androidx.annotation.VisibleForTesting
45
import com.auth0.android.authentication.AuthenticationAPIClient
56
import com.auth0.android.callback.Callback
6-
import com.auth0.android.request.internal.GsonProvider
7-
import com.auth0.android.request.internal.Jwt
87
import com.auth0.android.result.APICredentials
98
import com.auth0.android.result.Credentials
109
import com.auth0.android.result.SSOCredentials
1110
import com.auth0.android.result.UserProfile
1211
import com.auth0.android.util.Clock
1312
import java.util.*
14-
import kotlin.collections.component1
15-
import kotlin.collections.component2
1613

1714
/**
1815
* Base class meant to abstract common logic across Credentials Manager implementations.
@@ -36,7 +33,12 @@ public abstract class BaseCredentialsManager internal constructor(
3633

3734
@Throws(CredentialsManagerException::class)
3835
public abstract fun saveCredentials(credentials: Credentials)
39-
public abstract fun saveApiCredentials(apiCredentials: APICredentials, audience: String)
36+
public abstract fun saveApiCredentials(
37+
apiCredentials: APICredentials,
38+
audience: String,
39+
scope: String? = null
40+
)
41+
4042
public abstract fun getCredentials(callback: Callback<Credentials, CredentialsManagerException>)
4143
public abstract fun getSsoCredentials(
4244
parameters: Map<String, String>,
@@ -145,7 +147,7 @@ public abstract class BaseCredentialsManager internal constructor(
145147
public abstract val userProfile: UserProfile?
146148

147149
public abstract fun clearCredentials()
148-
public abstract fun clearApiCredentials(audience: String)
150+
public abstract fun clearApiCredentials(audience: String, scope: String? = null)
149151
public abstract fun hasValidCredentials(): Boolean
150152
public abstract fun hasValidCredentials(minTtl: Long): Boolean
151153

@@ -158,17 +160,24 @@ public abstract class BaseCredentialsManager internal constructor(
158160
*
159161
* @param storedScope the stored scope, separated by space characters.
160162
* @param requiredScope the required scope, separated by space characters.
163+
* @param ignoreOpenid whether to ignore the openid scope from the storedScope or not while comparing.
161164
* @return whether the scope are different or not
162165
*/
163-
protected fun hasScopeChanged(storedScope: String?, requiredScope: String?): Boolean {
166+
protected fun hasScopeChanged(
167+
storedScope: String?,
168+
requiredScope: String?,
169+
ignoreOpenid: Boolean = false
170+
): Boolean {
164171
if (requiredScope == null) {
165172
return false
166173
}
167-
val stored = storedScope.orEmpty().split(" ").toTypedArray()
168-
Arrays.sort(stored)
169-
val required = requiredScope.split(" ").toTypedArray()
170-
Arrays.sort(required)
171-
return !stored.contentEquals(required)
174+
val storedScopes =
175+
storedScope.orEmpty().split(" ").filter { it.isNotEmpty() }.toMutableSet()
176+
if (ignoreOpenid) {
177+
storedScopes.remove("openid")
178+
}
179+
val requiredScopes = requiredScope.split(" ").filter { it.isNotEmpty() }.toSet()
180+
return storedScopes != requiredScopes
172181
}
173182

174183
/**
@@ -196,4 +205,18 @@ public abstract class BaseCredentialsManager internal constructor(
196205
protected fun hasExpired(expiresAt: Long): Boolean {
197206
return expiresAt <= currentTimeInMillis
198207
}
199-
}
208+
209+
/**
210+
* Returns the key for storing the APICredentials in storage. Uses a combination of audience and scope.
211+
*
212+
* @param audience the audience of the credentials.
213+
* @param scope optional scope for the credentials.
214+
*/
215+
protected fun getAPICredentialsKey(audience: String, scope: String?): String {
216+
// Use audience if scope is null else use a combination of audience and scope
217+
if (scope == null) return audience
218+
val sortedScope = scope.split(" ").sorted().joinToString("::")
219+
return "$audience::${sortedScope}"
220+
221+
}
222+
}

auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,16 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
8181
* Stores the given [APICredentials] in the storage for the given audience.
8282
* @param apiCredentials the API Credentials to be stored
8383
* @param audience the audience for which the credentials are stored
84+
* @param scope the scope for which the credentials are stored
8485
*/
85-
override fun saveApiCredentials(apiCredentials: APICredentials, audience: String) {
86+
override fun saveApiCredentials(
87+
apiCredentials: APICredentials,
88+
audience: String,
89+
scope: String?
90+
) {
91+
val key = getAPICredentialsKey(audience, scope)
8692
gson.toJson(apiCredentials).let {
87-
storage.store(audience, it)
93+
storage.store(key, it)
8894
}
8995
}
9096

@@ -590,14 +596,23 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
590596
headers: Map<String, String>,
591597
callback: Callback<APICredentials, CredentialsManagerException>
592598
) {
599+
593600
serialExecutor.execute {
594601
//Check if existing api credentials are present and valid
595-
val apiCredentialsJson = storage.retrieveString(audience)
602+
val key = getAPICredentialsKey(audience, scope)
603+
val apiCredentialsJson = storage.retrieveString(key)
596604
apiCredentialsJson?.let {
597605
val apiCredentials = gson.fromJson(it, APICredentials::class.java)
598606
val willTokenExpire = willExpire(apiCredentials.expiresAt.time, minTtl.toLong())
599-
val scopeChanged = hasScopeChanged(apiCredentials.scope, scope)
607+
608+
val scopeChanged = hasScopeChanged(
609+
apiCredentials.scope,
610+
scope,
611+
ignoreOpenid = scope?.contains("openid") == false
612+
)
613+
600614
val hasExpired = hasExpired(apiCredentials.expiresAt.time)
615+
601616
if (!hasExpired && !willTokenExpire && !scopeChanged) {
602617
callback.onSuccess(apiCredentials)
603618
return@execute
@@ -641,7 +656,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
641656
val newApiCredentials = newCredentials.toAPICredentials()
642657
storage.store(KEY_REFRESH_TOKEN, updatedRefreshToken)
643658
storage.store(KEY_ID_TOKEN, newCredentials.idToken)
644-
saveApiCredentials(newApiCredentials, audience)
659+
saveApiCredentials(newApiCredentials, audience, scope)
645660
callback.onSuccess(newApiCredentials)
646661
} catch (error: AuthenticationException) {
647662
val exception = when {
@@ -702,10 +717,13 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
702717

703718
/**
704719
* Removes the credentials for the given audience from the storage if present.
720+
* @param audience Audience for which the [APICredentials] are stored
721+
* @param scope Optional scope for which the [APICredentials] are stored. If the credentials were initially fetched/stored with scope,
722+
* it is recommended to pass scope also while clearing them.
705723
*/
706-
override fun clearApiCredentials(audience: String) {
707-
storage.remove(audience)
708-
Log.d(TAG, "API Credentials for $audience were just removed from the storage")
724+
override fun clearApiCredentials(audience: String, scope: String?) {
725+
val key = getAPICredentialsKey(audience, scope)
726+
storage.remove(key)
709727
}
710728

711729
/**

auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -209,25 +209,26 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
209209
* Stores the given [APICredentials] in the storage for the given audience.
210210
* @param apiCredentials the API Credentials to be stored
211211
* @param audience the audience for which the credentials are stored
212+
* @param scope the scope for which the credentials are stored
212213
*/
213-
override fun saveApiCredentials(apiCredentials: APICredentials, audience: String) {
214+
override fun saveApiCredentials(
215+
apiCredentials: APICredentials,
216+
audience: String,
217+
scope: String?
218+
) {
219+
val key = getAPICredentialsKey(audience, scope)
214220
val json = gson.toJson(apiCredentials)
215221
try {
216222
val encrypted = crypto.encrypt(json.toByteArray())
217223
val encryptedEncoded = Base64.encodeToString(encrypted, Base64.DEFAULT)
218-
storage.store(audience, encryptedEncoded)
224+
storage.store(key, encryptedEncoded)
219225
} catch (e: IncompatibleDeviceException) {
220226
throw CredentialsManagerException(
221227
CredentialsManagerException.Code.INCOMPATIBLE_DEVICE,
222228
e
223229
)
224230
} catch (e: CryptoException) {
225-
/*
226-
* If the keys were invalidated in the call above a good new pair is going to be available
227-
* to use on the next call. We clear any existing credentials so #hasValidCredentials returns
228-
* a true value. Retrying this operation will succeed.
229-
*/
230-
clearApiCredentials(audience)
231+
clearApiCredentials(audience, scope)
231232
throw CredentialsManagerException(
232233
CredentialsManagerException.Code.CRYPTO_EXCEPTION,
233234
e
@@ -331,12 +332,10 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
331332

332333
public override val userProfile: UserProfile?
333334
get() {
334-
val credentials: Credentials? = getExistingCredentials()
335-
// Handle null credentials gracefully
336-
if (credentials == null) {
337-
return null
338-
}
339-
return credentials.user
335+
return runCatching {
336+
val credentials: Credentials = getExistingCredentials()
337+
return credentials.user
338+
}.getOrNull()
340339
}
341340

342341
/**
@@ -765,10 +764,13 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
765764

766765
/**
767766
* Removes the credentials for the given audience from the storage if present.
767+
* @param audience Audience for which the [APICredentials] are stored
768+
* @param scope Optional scope for which the [APICredentials] are stored. If the credentials were initially fetched/stored with scope,
769+
* it is recommended to pass scope also while clearing them.
768770
*/
769-
override fun clearApiCredentials(audience: String) {
770-
storage.remove(audience)
771-
Log.d(TAG, "API Credentials for $audience were just removed from the storage")
771+
override fun clearApiCredentials(audience: String, scope: String?) {
772+
val key = getAPICredentialsKey(audience, scope)
773+
storage.remove(key)
772774
}
773775

774776
/**
@@ -965,7 +967,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
965967
callback: Callback<APICredentials, CredentialsManagerException>
966968
) {
967969
serialExecutor.execute {
968-
val encryptedEncodedJson = storage.retrieveString(audience)
970+
val encryptedEncodedJson = storage.retrieveString(getAPICredentialsKey(audience, scope))
969971
//Check if existing api credentials are present and valid
970972

971973
encryptedEncodedJson?.let { encryptedEncoded ->
@@ -981,8 +983,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
981983
)
982984
return@execute
983985
} catch (e: CryptoException) {
984-
//If keys were invalidated, existing credentials will not be recoverable.
985-
clearApiCredentials(audience)
986+
clearApiCredentials(audience, scope)
986987
callback.onFailure(
987988
CredentialsManagerException(
988989
CredentialsManagerException.Code.CRYPTO_EXCEPTION,
@@ -996,7 +997,10 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
996997

997998
val expiresAt = apiCredentials.expiresAt.time
998999
val willAccessTokenExpire = willExpire(expiresAt, minTtl.toLong())
999-
val scopeChanged = hasScopeChanged(apiCredentials.scope, scope)
1000+
val scopeChanged = hasScopeChanged(
1001+
apiCredentials.scope, scope,
1002+
ignoreOpenid = scope?.contains("openid") == false
1003+
)
10001004
val hasExpired = hasExpired(apiCredentials.expiresAt.time)
10011005
if (!hasExpired && !willAccessTokenExpire && !scopeChanged) {
10021006
callback.onSuccess(apiCredentials)
@@ -1051,7 +1055,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
10511055
idToken = newCredentials.idToken
10521056
)
10531057
)
1054-
saveApiCredentials(newApiCredentials, audience)
1058+
saveApiCredentials(newApiCredentials, audience, scope)
10551059
callback.onSuccess(newApiCredentials)
10561060

10571061
} catch (error: AuthenticationException) {
@@ -1200,6 +1204,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
12001204

12011205
val policy = localAuthenticationOptions?.policy ?: BiometricPolicy.Always
12021206
return when (policy) {
1207+
12031208
is BiometricPolicy.Session,
12041209
is BiometricPolicy.AppLifecycle -> {
12051210
val timeoutMillis = when (policy) {

0 commit comments

Comments
 (0)