Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
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
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.
Expand All @@ -36,7 +33,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? = null
)

public abstract fun getCredentials(callback: Callback<Credentials, CredentialsManagerException>)
public abstract fun getSsoCredentials(
parameters: Map<String, String>,
Expand Down Expand Up @@ -145,7 +147,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

Expand All @@ -158,17 +160,24 @@ 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()
Arrays.sort(stored)
val required = requiredScope.split(" ").toTypedArray()
Arrays.sort(required)
return !stored.contentEquals(required)
val storedScopes =
storedScope.orEmpty().split(" ").filter { it.isNotEmpty() }.toMutableSet()
if (ignoreOpenid) {
storedScopes.remove("openid")
}
val requiredScopes = requiredScope.split(" ").filter { it.isNotEmpty() }.toSet()
return storedScopes != requiredScopes
}

/**
Expand Down Expand Up @@ -196,4 +205,18 @@ 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
if (scope == null) return audience
val sortedScope = scope.split(" ").sorted().joinToString("::")
return "$audience::${sortedScope}"

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -590,14 +596,23 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
headers: Map<String, String>,
callback: Callback<APICredentials, CredentialsManagerException>
) {

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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -702,10 +717,13 @@ 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) {
storage.remove(audience)
Log.d(TAG, "API Credentials for $audience were just removed from the storage")
override fun clearApiCredentials(audience: String, scope: String?) {
val key = getAPICredentialsKey(audience, scope)
storage.remove(key)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,25 +209,26 @@ 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,
e
)
} catch (e: CryptoException) {
/*
* If the keys were invalidated in the call above a good new pair is going to be available
* 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
Expand Down Expand Up @@ -331,12 +332,10 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT

public override val userProfile: UserProfile?
get() {
val credentials: Credentials? = getExistingCredentials()
// Handle null credentials gracefully
if (credentials == null) {
return null
}
return credentials.user
return runCatching {
val credentials: Credentials = getExistingCredentials()
return credentials.user
}.getOrNull()
}

/**
Expand Down Expand Up @@ -765,10 +764,13 @@ 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) {
storage.remove(audience)
Log.d(TAG, "API Credentials for $audience were just removed from the storage")
override fun clearApiCredentials(audience: String, scope: String?) {
val key = getAPICredentialsKey(audience, scope)
storage.remove(key)
}

/**
Expand Down Expand Up @@ -965,7 +967,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
callback: Callback<APICredentials, CredentialsManagerException>
) {
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 ->
Expand All @@ -981,8 +983,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,
Expand All @@ -996,7 +997,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)
Expand Down Expand Up @@ -1051,7 +1055,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
idToken = newCredentials.idToken
)
)
saveApiCredentials(newApiCredentials, audience)
saveApiCredentials(newApiCredentials, audience, scope)
callback.onSuccess(newApiCredentials)

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

val policy = localAuthenticationOptions?.policy ?: BiometricPolicy.Always
return when (policy) {

is BiometricPolicy.Session,
is BiometricPolicy.AppLifecycle -> {
val timeoutMillis = when (policy) {
Expand Down
Loading