Skip to content

Commit

Permalink
Implement Thread Safe Credential Manager (#542)
Browse files Browse the repository at this point in the history
* Implement Thread Safe Credential Manager

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

Co-authored-by: Steve Hobbs <steve.hobbs@auth0.com>

* Corrected grammar in Exception message

* Test if save credentials method is marked synchronized

* provide thread safety information in README

* Serial Executor is made into a setter

* Remove debug messages used while development

* Use standard non expiring mock

* Make serial executor private variable in respective cred managers

Co-authored-by: Steve Hobbs <steve.hobbs@auth0.com>
  • Loading branch information
poovamraj and Steve Hobbs authored Feb 25, 2022
1 parent cade86a commit 3ea50a3
Show file tree
Hide file tree
Showing 6 changed files with 267 additions and 261 deletions.
11 changes: 4 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -601,9 +601,8 @@ authentication
manager.saveCredentials(credentials)
}
})
```

**Note:** This method is not thread-safe. Attempting to save credentials multiple times in a short period or from different threads can cause issues like invalidating valid stored tokens.
```
**Note:** This method has been made thread-safe after version 2.7.0.

3. **Check credentials existence:**
There are cases were you just want to check if a user session is still valid (i.e. to know if you should present the login screen or the main screen). For convenience, we include a `hasValidCredentials` method that can let you know in advance if a non-expired token is available without making an additional network call. The same rules of the `getCredentials` method apply:
Expand All @@ -625,10 +624,8 @@ manager.getCredentials(object : Callback<Credentials, CredentialsManagerExceptio
// Use the credentials
}
})
```

**Note:** This method is not thread-safe. In the scenario where the stored credentials have expired and a `refresh_token` is available, the newly obtained tokens are automatically saved for you by the Credentials Manager. If you're using _Refresh Token Rotation_ you should avoid calling this method concurrently as it might result in more than one renew request being fired, and only the first one succeeding.

```
**Note:** In the scenario where the stored credentials have expired and a `refresh_token` is available, the newly obtained tokens are automatically saved for you by the Credentials Manager. This method has been made thread-safe after version 2.7.0.

5. **Clear credentials:**
When you want to log the user out:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import com.auth0.android.callback.Callback
import com.auth0.android.result.Credentials
import com.auth0.android.util.Clock
import java.util.*
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import kotlin.math.min

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@ import android.text.TextUtils
import androidx.annotation.VisibleForTesting
import com.auth0.android.authentication.AuthenticationAPIClient
import com.auth0.android.authentication.AuthenticationException
import com.auth0.android.callback.AuthenticationCallback
import com.auth0.android.callback.Callback
import com.auth0.android.result.Credentials
import java.util.*
import java.util.concurrent.Executor
import java.util.concurrent.Executors

/**
* Class that handles credentials and allows to save and retrieve them.
*/
public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal constructor(
authenticationClient: AuthenticationAPIClient,
storage: Storage,
jwtDecoder: JWTDecoder
jwtDecoder: JWTDecoder,
private val serialExecutor: Executor
) : BaseCredentialsManager(authenticationClient, storage, jwtDecoder) {
/**
* Creates a new instance of the manager that will store the credentials in the given Storage.
Expand All @@ -26,7 +28,8 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
public constructor(authenticationClient: AuthenticationAPIClient, storage: Storage) : this(
authenticationClient,
storage,
JWTDecoder()
JWTDecoder(),
Executors.newSingleThreadExecutor()
)

/**
Expand Down Expand Up @@ -92,50 +95,51 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
parameters: Map<String, String>,
callback: Callback<Credentials, CredentialsManagerException>
) {
val accessToken = storage.retrieveString(KEY_ACCESS_TOKEN)
val refreshToken = storage.retrieveString(KEY_REFRESH_TOKEN)
val idToken = storage.retrieveString(KEY_ID_TOKEN)
val tokenType = storage.retrieveString(KEY_TOKEN_TYPE)
val expiresAt = storage.retrieveLong(KEY_EXPIRES_AT)
val storedScope = storage.retrieveString(KEY_SCOPE)
var cacheExpiresAt = storage.retrieveLong(KEY_CACHE_EXPIRES_AT)
if (cacheExpiresAt == null) {
cacheExpiresAt = expiresAt
}
val hasEmptyCredentials =
TextUtils.isEmpty(accessToken) && TextUtils.isEmpty(idToken) || expiresAt == null
if (hasEmptyCredentials) {
callback.onFailure(CredentialsManagerException("No Credentials were previously set."))
return
}
val hasEitherExpired = hasExpired(cacheExpiresAt!!)
val willAccessTokenExpire = willExpire(expiresAt!!, minTtl.toLong())
val scopeChanged = hasScopeChanged(storedScope, scope)
if (!hasEitherExpired && !willAccessTokenExpire && !scopeChanged) {
callback.onSuccess(
recreateCredentials(
idToken.orEmpty(),
accessToken.orEmpty(),
tokenType.orEmpty(),
refreshToken,
Date(expiresAt),
storedScope
serialExecutor.execute {
val accessToken = storage.retrieveString(KEY_ACCESS_TOKEN)
val refreshToken = storage.retrieveString(KEY_REFRESH_TOKEN)
val idToken = storage.retrieveString(KEY_ID_TOKEN)
val tokenType = storage.retrieveString(KEY_TOKEN_TYPE)
val expiresAt = storage.retrieveLong(KEY_EXPIRES_AT)
val storedScope = storage.retrieveString(KEY_SCOPE)
var cacheExpiresAt = storage.retrieveLong(KEY_CACHE_EXPIRES_AT)
if (cacheExpiresAt == null) {
cacheExpiresAt = expiresAt
}
val hasEmptyCredentials =
TextUtils.isEmpty(accessToken) && TextUtils.isEmpty(idToken) || expiresAt == null
if (hasEmptyCredentials) {
callback.onFailure(CredentialsManagerException("No Credentials were previously set."))
return@execute
}
val hasEitherExpired = hasExpired(cacheExpiresAt!!)
val willAccessTokenExpire = willExpire(expiresAt!!, minTtl.toLong())
val scopeChanged = hasScopeChanged(storedScope, scope)
if (!hasEitherExpired && !willAccessTokenExpire && !scopeChanged) {
callback.onSuccess(
recreateCredentials(
idToken.orEmpty(),
accessToken.orEmpty(),
tokenType.orEmpty(),
refreshToken,
Date(expiresAt),
storedScope
)
)
)
return
}
if (refreshToken == null) {
callback.onFailure(CredentialsManagerException("Credentials need to be renewed but no Refresh Token is available to renew them."))
return
}
val request = authenticationClient.renewAuth(refreshToken)
request.addParameters(parameters)
if (scope != null) {
request.addParameter("scope", scope)
}
return@execute
}
if (refreshToken == null) {
callback.onFailure(CredentialsManagerException("Credentials need to be renewed but no Refresh Token is available to renew them."))
return@execute
}
val request = authenticationClient.renewAuth(refreshToken)
request.addParameters(parameters)
if (scope != null) {
request.addParameter("scope", scope)
}

request.start(object : AuthenticationCallback<Credentials> {
override fun onSuccess(fresh: Credentials) {
try {
val fresh = request.execute()
val expiresAt = fresh.expiresAt.time
val willAccessTokenExpire = willExpire(expiresAt, minTtl.toLong())
if (willAccessTokenExpire) {
Expand All @@ -149,7 +153,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
)
)
callback.onFailure(wrongTtlException)
return
return@execute
}

// non-empty refresh token for refresh token rotation scenarios
Expand All @@ -165,17 +169,15 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
)
saveCredentials(credentials)
callback.onSuccess(credentials)
}

override fun onFailure(error: AuthenticationException) {
} catch (error: AuthenticationException) {
callback.onFailure(
CredentialsManagerException(
"An error occurred while trying to use the Refresh Token to renew the Credentials.",
error
)
)
}
})
}
}

/**
Expand Down
Loading

0 comments on commit 3ea50a3

Please sign in to comment.