Skip to content

Commit

Permalink
Added forceRefresh option to getCredentials (auth0#637)
Browse files Browse the repository at this point in the history
Co-authored-by: Rita Zerrizuela <[email protected]>
  • Loading branch information
poovamraj and Widcket committed Mar 16, 2023
1 parent 3dfe35b commit 3ec961f
Show file tree
Hide file tree
Showing 4 changed files with 255 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,35 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
scope: String?,
minTtl: Int,
parameters: Map<String, String>
): Credentials {
return awaitCredentials(scope, minTtl, parameters, false)
}

/**
* Retrieves the credentials from the storage and refresh them if they have already expired.
* It will throw [CredentialsManagerException] if the saved access_token or id_token is null,
* or if the tokens have already expired and the refresh_token is null.
* This is a Coroutine that is exposed only for Kotlin.
*
* @param scope the scope to request for the access token. If null is passed, the previous scope will be kept.
* @param minTtl the minimum time in seconds that the access token should last before expiration.
* @param parameters additional parameters to send in the request to refresh expired credentials.
* @param forceRefresh this will avoid returning the existing credentials and retrieves a new one even if valid credentials exist.
*/
@JvmSynthetic
@Throws(CredentialsManagerException::class)
public suspend fun awaitCredentials(
scope: String?,
minTtl: Int,
parameters: Map<String, String>,
forceRefresh: Boolean
): Credentials {
return suspendCancellableCoroutine { continuation ->
getCredentials(
scope,
minTtl,
parameters,
forceRefresh,
object : Callback<Credentials, CredentialsManagerException> {
override fun onSuccess(result: Credentials) {
continuation.resume(result)
Expand Down Expand Up @@ -157,6 +180,27 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
minTtl: Int,
parameters: Map<String, String>,
callback: Callback<Credentials, CredentialsManagerException>
) {
getCredentials(scope, minTtl, parameters, false, callback)
}

/**
* Retrieves the credentials from the storage and refresh them if they have already expired.
* It will fail with [CredentialsManagerException] if the saved access_token or id_token is null,
* or if the tokens have already expired and the refresh_token is null.
*
* @param scope the scope to request for the access token. If null is passed, the previous scope will be kept.
* @param minTtl the minimum time in seconds that the access token should last before expiration.
* @param parameters additional parameters to send in the request to refresh expired credentials.
* @param forceRefresh this will avoid returning the existing credentials and retrieves a new one even if valid credentials exist.
* @param callback the callback that will receive a valid [Credentials] or the [CredentialsManagerException].
*/
public fun getCredentials(
scope: String?,
minTtl: Int,
parameters: Map<String, String>,
forceRefresh: Boolean,
callback: Callback<Credentials, CredentialsManagerException>
) {
serialExecutor.execute {
val accessToken = storage.retrieveString(KEY_ACCESS_TOKEN)
Expand All @@ -173,7 +217,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
}
val willAccessTokenExpire = willExpire(expiresAt!!, minTtl.toLong())
val scopeChanged = hasScopeChanged(storedScope, scope)
if (!willAccessTokenExpire && !scopeChanged) {
if (!forceRefresh && !willAccessTokenExpire && !scopeChanged) {
callback.onSuccess(
recreateCredentials(
idToken.orEmpty(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
private var authIntent: Intent? = null
private var scope: String? = null
private var minTtl = 0
private var forceRefresh = false

/**
* Creates a new SecureCredentialsManager to handle Credentials
Expand Down Expand Up @@ -145,7 +146,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
return false
}
if (resultCode == Activity.RESULT_OK) {
continueGetCredentials(scope, minTtl, emptyMap(), decryptCallback!!)
continueGetCredentials(scope, minTtl, emptyMap(), forceRefresh, decryptCallback!!)
} else {
decryptCallback!!.onFailure(CredentialsManagerException("The user didn't pass the authentication challenge."))
decryptCallback = null
Expand Down Expand Up @@ -254,12 +255,39 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
scope: String?,
minTtl: Int,
parameters: Map<String, String>
): Credentials {
return awaitCredentials(scope, minTtl, parameters, false)
}

/**
* Tries to obtain the credentials from the Storage. The method will return [Credentials].
* If something unexpected happens, then [CredentialsManagerException] exception will be thrown. Some devices are not compatible
* at all with the cryptographic implementation and will have [CredentialsManagerException.isDeviceIncompatible] return true.
* This is a Coroutine that is exposed only for Kotlin.
*
* If a LockScreen is setup and [SecureCredentialsManager.requireAuthentication] was called, the user will be asked to authenticate before accessing
* the credentials. Your activity must override the [Activity.onActivityResult] method and call
* [SecureCredentialsManager.checkAuthenticationResult] with the received values.
*
* @param scope the scope to request for the access token. If null is passed, the previous scope will be kept.
* @param minTtl the minimum time in seconds that the access token should last before expiration.
* @param parameters additional parameters to send in the request to refresh expired credentials.
* @param forceRefresh this will avoid returning the existing credentials and retrieves a new one even if valid credentials exist.
*/
@JvmSynthetic
@Throws(CredentialsManagerException::class)
public suspend fun awaitCredentials(
scope: String?,
minTtl: Int,
parameters: Map<String, String>,
forceRefresh: Boolean,
): Credentials {
return suspendCancellableCoroutine { continuation ->
getCredentials(
scope,
minTtl,
parameters,
forceRefresh,
object : Callback<Credentials, CredentialsManagerException> {
override fun onSuccess(result: Credentials) {
continuation.resume(result)
Expand Down Expand Up @@ -330,6 +358,32 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
minTtl: Int,
parameters: Map<String, String>,
callback: Callback<Credentials, CredentialsManagerException>
) {
getCredentials(scope, minTtl, parameters, false, callback)
}

/**
* Tries to obtain the credentials from the Storage. The callback's [Callback.onSuccess] method will be called with the result.
* If something unexpected happens, the [Callback.onFailure] method will be called with the error. Some devices are not compatible
* at all with the cryptographic implementation and will have [CredentialsManagerException.isDeviceIncompatible] return true.
*
*
* If a LockScreen is setup and [SecureCredentialsManager.requireAuthentication] was called, the user will be asked to authenticate before accessing
* the credentials. Your activity must override the [Activity.onActivityResult] method and call
* [SecureCredentialsManager.checkAuthenticationResult] with the received values.
*
* @param scope the scope to request for the access token. If null is passed, the previous scope will be kept.
* @param minTtl the minimum time in seconds that the access token should last before expiration.
* @param parameters additional parameters to send in the request to refresh expired credentials.
* @param forceRefresh this will avoid returning the existing credentials and retrieves a new one even if valid credentials exist.
* @param callback the callback to receive the result in.
*/
public fun getCredentials(
scope: String?,
minTtl: Int,
parameters: Map<String, String>,
forceRefresh: Boolean,
callback: Callback<Credentials, CredentialsManagerException>
) {
if (!hasValidCredentials(minTtl.toLong())) {
callback.onFailure(CredentialsManagerException("No Credentials were previously set."))
Expand All @@ -343,11 +397,12 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
decryptCallback = callback
this.scope = scope
this.minTtl = minTtl
this.forceRefresh = forceRefresh
activityResultContract?.launch(authIntent)
?: activity?.startActivityForResult(authIntent, authenticationRequestCode)
return
}
continueGetCredentials(scope, minTtl, parameters, callback)
continueGetCredentials(scope, minTtl, parameters, forceRefresh, callback)
}

/**
Expand Down Expand Up @@ -396,6 +451,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
scope: String?,
minTtl: Int,
parameters: Map<String, String>,
forceRefresh: Boolean,
callback: Callback<Credentials, CredentialsManagerException>
) {
serialExecutor.execute {
Expand Down Expand Up @@ -456,7 +512,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
}
val willAccessTokenExpire = willExpire(expiresAt, minTtl.toLong())
val scopeChanged = hasScopeChanged(credentials.scope, scope)
if (!willAccessTokenExpire && !scopeChanged) {
if (!forceRefresh && !willAccessTokenExpire && !scopeChanged) {
callback.onSuccess(credentials)
decryptCallback = null
return@execute
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,84 @@ public class CredentialsManagerTest {
verify(request).execute()
}

@Test
public fun shouldReturnNewCredentialsIfForced() {
Mockito.`when`(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken")
Mockito.`when`(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken")
Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken")
val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS // non expired credentials
val parameters = mapOf(
"client_id" to "new Client ID",
"phone" to "+1 (777) 124-1588"
)
Mockito.`when`(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime)
Mockito.`when`(storage.retrieveLong("com.auth0.cache_expires_at"))
.thenReturn(expirationTime)
Mockito.`when`(storage.retrieveString("com.auth0.scope")).thenReturn("oldscope")
Mockito.`when`(
client.renewAuth("refreshToken")
).thenReturn(request)

val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS)
val jwtMock = mock<Jwt>()
Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate)
Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock)
val expectedCredentials =
Credentials("newId", "newAccess", "newType", "newRefresh", newDate, "oldscope")
Mockito.`when`(request.execute()).thenReturn(expectedCredentials)
manager.getCredentials(
scope = "oldscope",
minTtl = 0,
parameters = parameters,
forceRefresh = true,
callback = callback
)
verify(request).execute()

verify(callback).onSuccess(credentialsCaptor.capture())
val retrievedCredentials = credentialsCaptor.firstValue
MatcherAssert.assertThat(retrievedCredentials, Is.`is`(Matchers.notNullValue()))
MatcherAssert.assertThat(retrievedCredentials.accessToken, Is.`is`(expectedCredentials.accessToken))
MatcherAssert.assertThat(retrievedCredentials.idToken, Is.`is`(expectedCredentials.idToken))
MatcherAssert.assertThat(retrievedCredentials.refreshToken, Is.`is`(expectedCredentials.refreshToken))
MatcherAssert.assertThat(retrievedCredentials.type, Is.`is`(expectedCredentials.type))
MatcherAssert.assertThat(retrievedCredentials.expiresAt, Is.`is`(Matchers.notNullValue()))
MatcherAssert.assertThat(retrievedCredentials.expiresAt.time, Is.`is`(expectedCredentials.expiresAt.time))
MatcherAssert.assertThat(retrievedCredentials.scope, Is.`is`(expectedCredentials.scope))
}

@Test
public fun shouldReturnSameCredentialsIfNotForced() {
verifyNoMoreInteractions(client)
Mockito.`when`(storage.retrieveString("com.auth0.id_token")).thenReturn(null)
Mockito.`when`(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken")
Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken")
Mockito.`when`(storage.retrieveString("com.auth0.token_type")).thenReturn("type")
val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS
Mockito.`when`(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime)
Mockito.`when`(storage.retrieveLong("com.auth0.cache_expires_at"))
.thenReturn(expirationTime)
Mockito.`when`(storage.retrieveString("com.auth0.scope")).thenReturn("scope")
manager.getCredentials("scope",
0,
emptyMap(),
false,
callback
)
verify(callback).onSuccess(
credentialsCaptor.capture()
)
val retrievedCredentials = credentialsCaptor.firstValue
MatcherAssert.assertThat(retrievedCredentials, Is.`is`(Matchers.notNullValue()))
MatcherAssert.assertThat(retrievedCredentials.accessToken, Is.`is`("accessToken"))
MatcherAssert.assertThat(retrievedCredentials.idToken, Is.`is`(""))
MatcherAssert.assertThat(retrievedCredentials.refreshToken, Is.`is`("refreshToken"))
MatcherAssert.assertThat(retrievedCredentials.type, Is.`is`("type"))
MatcherAssert.assertThat(retrievedCredentials.expiresAt, Is.`is`(Matchers.notNullValue()))
MatcherAssert.assertThat(retrievedCredentials.expiresAt.time, Is.`is`(expirationTime))
MatcherAssert.assertThat(retrievedCredentials.scope, Is.`is`("scope"))
}

private fun prepareJwtDecoderMock(expiresAt: Date?) {
val jwtMock = mock<Jwt>()
Mockito.`when`(jwtMock.expiresAt).thenReturn(expiresAt)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1684,6 +1684,79 @@ public class SecureCredentialsManagerTest {
verify(request).execute()
}

@Test
public fun shouldReturnNewCredentialsIfForced() {
val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) // non expired credentials
insertTestCredentials(
hasIdToken = false,
hasAccessToken = true,
hasRefreshToken = true,
willExpireAt = expiresAt,
scope = "scope"
) // "scope" is set
val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS + ONE_HOUR_SECONDS * 1000)
val jwtMock = mock<Jwt>()
val parameters = mapOf(
"client_id" to "new Client ID",
"phone" to "+1 (777) 124-1588"
)
Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate)
Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock)
Mockito.`when`(
client.renewAuth("refreshToken")
).thenReturn(request)

val expectedCredentials =
Credentials("newId", "newAccess", "newType", "newRefresh", newDate, "oldscope")
Mockito.`when`(request.execute()).thenReturn(expectedCredentials)
val expectedJson = gson.toJson(expectedCredentials)
Mockito.`when`(crypto.encrypt(expectedJson.toByteArray()))
.thenReturn(expectedJson.toByteArray())
manager.getCredentials(
scope = "scope",
minTtl = 0,
parameters = parameters,
forceRefresh = true,
callback = callback
)
verify(request).execute()
verify(callback).onSuccess(credentialsCaptor.capture())
val retrievedCredentials = credentialsCaptor.firstValue
MatcherAssert.assertThat(retrievedCredentials, Is.`is`(Matchers.notNullValue()))
MatcherAssert.assertThat(retrievedCredentials.accessToken, Is.`is`(expectedCredentials.accessToken))
MatcherAssert.assertThat(retrievedCredentials.idToken, Is.`is`(expectedCredentials.idToken))
MatcherAssert.assertThat(retrievedCredentials.refreshToken, Is.`is`(expectedCredentials.refreshToken))
MatcherAssert.assertThat(retrievedCredentials.type, Is.`is`(expectedCredentials.type))
MatcherAssert.assertThat(retrievedCredentials.expiresAt, Is.`is`(Matchers.notNullValue()))
MatcherAssert.assertThat(retrievedCredentials.expiresAt.time, Is.`is`(expectedCredentials.expiresAt.time))
MatcherAssert.assertThat(retrievedCredentials.scope, Is.`is`(expectedCredentials.scope))
}

@Test
public fun shouldReturnSameCredentialsIfNotForced() {
verifyNoMoreInteractions(client)
val expiresAt = Date(CredentialsMock.CURRENT_TIME_MS + ONE_HOUR_SECONDS * 1000)
insertTestCredentials(true, true, true, expiresAt, "scope")
manager.getCredentials("scope",
0,
emptyMap(),
false,
callback
)
verify(callback).onSuccess(
credentialsCaptor.capture()
)
val retrievedCredentials = credentialsCaptor.firstValue
MatcherAssert.assertThat(retrievedCredentials, Is.`is`(Matchers.notNullValue()))
MatcherAssert.assertThat(retrievedCredentials.accessToken, Is.`is`("accessToken"))
MatcherAssert.assertThat(retrievedCredentials.idToken, Is.`is`("idToken"))
MatcherAssert.assertThat(retrievedCredentials.refreshToken, Is.`is`("refreshToken"))
MatcherAssert.assertThat(retrievedCredentials.type, Is.`is`("type"))
MatcherAssert.assertThat(retrievedCredentials.expiresAt, Is.`is`(Matchers.notNullValue()))
MatcherAssert.assertThat(retrievedCredentials.expiresAt.time, Is.`is`(expiresAt.time))
MatcherAssert.assertThat(retrievedCredentials.scope, Is.`is`("scope"))
}

@Test
public fun shouldBeMarkedSynchronous(){
val method =
Expand Down

0 comments on commit 3ec961f

Please sign in to comment.