@@ -6,8 +6,12 @@ import androidx.annotation.VisibleForTesting
66import com.auth0.android.authentication.AuthenticationAPIClient
77import com.auth0.android.authentication.AuthenticationException
88import com.auth0.android.callback.Callback
9+ import com.auth0.android.request.internal.GsonProvider
10+ import com.auth0.android.result.APICredentials
911import com.auth0.android.result.Credentials
1012import com.auth0.android.result.SSOCredentials
13+ import com.auth0.android.result.toAPICredentials
14+ import com.google.gson.Gson
1115import kotlinx.coroutines.suspendCancellableCoroutine
1216import java.util.*
1317import java.util.concurrent.Executor
@@ -24,6 +28,9 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
2428 jwtDecoder : JWTDecoder ,
2529 private val serialExecutor : Executor
2630) : BaseCredentialsManager(authenticationClient, storage, jwtDecoder) {
31+
32+ private val gson: Gson = GsonProvider .gson
33+
2734 /* *
2835 * Creates a new instance of the manager that will store the credentials in the given Storage.
2936 *
@@ -55,6 +62,17 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
5562 storage.store(LEGACY_KEY_CACHE_EXPIRES_AT , credentials.expiresAt.time)
5663 }
5764
65+ /* *
66+ * Stores the given [APICredentials] in the storage for the given audience.
67+ * @param apiCredentials the API Credentials to be stored
68+ * @param audience the audience for which the credentials are stored
69+ */
70+ override fun saveApiCredentials (apiCredentials : APICredentials , audience : String ) {
71+ gson.toJson(apiCredentials).let {
72+ storage.store(audience, it)
73+ }
74+ }
75+
5876 /* *
5977 * Creates a new request to exchange a refresh token for a session transfer token that can be used to perform web single sign-on.
6078 *
@@ -305,6 +323,44 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
305323 }
306324 }
307325
326+ /* *
327+ * Retrieves API credentials from storage and automatically renews them using the refresh token if the access
328+ * token is expired. Otherwise, the retrieved API credentials will be returned as they are still valid.
329+ *
330+ * If there are no stored API credentials, the refresh token will be exchanged for a new set of API credentials.
331+ * New or renewed API credentials will be automatically persisted in storage.
332+ *
333+ * @param audience Identifier of the API that your application is requesting access to.
334+ * @param scope the scope to request for the access token. If null is passed, the previous scope will be kept.
335+ * @param minTtl the minimum time in seconds that the access token should last before expiration.
336+ * @param parameters additional parameters to send in the request to refresh expired credentials.
337+ * @param headers additional headers to send in the request to refresh expired credentials.
338+ */
339+ @JvmSynthetic
340+ @Throws(CredentialsManagerException ::class )
341+ override suspend fun awaitApiCredentials (
342+ audience : String ,
343+ scope : String? ,
344+ minTtl : Int ,
345+ parameters : Map <String , String >,
346+ headers : Map <String , String >
347+ ): APICredentials {
348+ return suspendCancellableCoroutine { continuation ->
349+ getApiCredentials(
350+ audience, scope, minTtl, parameters, headers,
351+ object : Callback <APICredentials , CredentialsManagerException > {
352+ override fun onSuccess (result : APICredentials ) {
353+ continuation.resume(result)
354+ }
355+
356+ override fun onFailure (error : CredentialsManagerException ) {
357+ continuation.resumeWithException(error)
358+ }
359+ }
360+ )
361+ }
362+ }
363+
308364 /* *
309365 * Retrieves the credentials from the storage and refresh them if they have already expired.
310366 * It will fail with [CredentialsManagerException] if the saved access_token or id_token is null,
@@ -496,6 +552,99 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
496552 }
497553 }
498554
555+
556+ /* *
557+ * Retrieves API credentials from storage and automatically renews them using the refresh token if the access
558+ * token is expired. Otherwise, the retrieved API credentials will be returned via the success callback as they are still valid.
559+ *
560+ * If there are no stored API credentials, the refresh token will be exchanged for a new set of API credentials.
561+ * New or renewed API credentials will be automatically persisted in storage.
562+ *
563+ * @param audience Identifier of the API that your application is requesting access to.
564+ * @param scope the scope to request for the access token. If null is passed, the previous scope will be kept.
565+ * @param minTtl the minimum time in seconds that the access token should last before expiration.
566+ * @param parameters additional parameters to send in the request to refresh expired credentials.
567+ * @param headers headers to use when exchanging a refresh token for API credentials.
568+ * @param callback the callback that will receive a valid [Credentials] or the [CredentialsManagerException].
569+ */
570+ override fun getApiCredentials (
571+ audience : String ,
572+ scope : String? ,
573+ minTtl : Int ,
574+ parameters : Map <String , String >,
575+ headers : Map <String , String >,
576+ callback : Callback <APICredentials , CredentialsManagerException >
577+ ) {
578+ serialExecutor.execute {
579+ // Check if existing api credentials are present and valid
580+ val apiCredentialsJson = storage.retrieveString(audience)
581+ apiCredentialsJson?.let {
582+ val apiCredentials = gson.fromJson(it, APICredentials ::class .java)
583+ val willTokenExpire = willExpire(apiCredentials.expiresAt.time, minTtl.toLong())
584+ val scopeChanged = hasScopeChanged(apiCredentials.scope, scope)
585+ val hasExpired = hasExpired(apiCredentials.expiresAt.time)
586+ if (! hasExpired && ! willTokenExpire && ! scopeChanged) {
587+ callback.onSuccess(apiCredentials)
588+ return @execute
589+ }
590+ }
591+ // Check if refresh token exists or not
592+ val refreshToken = storage.retrieveString(KEY_REFRESH_TOKEN )
593+ if (refreshToken == null ) {
594+ callback.onFailure(CredentialsManagerException .NO_REFRESH_TOKEN )
595+ return @execute
596+ }
597+
598+ val request = authenticationClient.renewAuth(refreshToken, audience, scope)
599+ request.addParameters(parameters)
600+
601+ for (header in headers) {
602+ request.addHeader(header.key, header.value)
603+ }
604+
605+ try {
606+ val newCredentials = request.execute()
607+ val expiresAt = newCredentials.expiresAt.time
608+ val willAccessTokenExpire = willExpire(expiresAt, minTtl.toLong())
609+ if (willAccessTokenExpire) {
610+ val tokenLifetime = (expiresAt - currentTimeInMillis - minTtl * 1000 ) / - 1000
611+ val wrongTtlException = CredentialsManagerException (
612+ CredentialsManagerException .Code .LARGE_MIN_TTL , String .format(
613+ Locale .getDefault(),
614+ " The lifetime of the renewed Access Token (%d) is less than the minTTL requested (%d). Increase the 'Token Expiration' setting of your Auth0 API in the dashboard, or request a lower minTTL." ,
615+ tokenLifetime,
616+ minTtl
617+ )
618+ )
619+ callback.onFailure(wrongTtlException)
620+ return @execute
621+ }
622+
623+ // non-empty refresh token for refresh token rotation scenarios
624+ val updatedRefreshToken =
625+ if (TextUtils .isEmpty(newCredentials.refreshToken)) refreshToken else newCredentials.refreshToken
626+ val newApiCredentials = newCredentials.toAPICredentials()
627+ storage.store(KEY_REFRESH_TOKEN , updatedRefreshToken)
628+ storage.store(KEY_ID_TOKEN , newCredentials.idToken)
629+ saveApiCredentials(newApiCredentials, audience)
630+ callback.onSuccess(newApiCredentials)
631+ } catch (error: AuthenticationException ) {
632+ val exception = when {
633+ error.isRefreshTokenDeleted || error.isInvalidRefreshToken -> CredentialsManagerException .Code .RENEW_FAILED
634+
635+ error.isNetworkError -> CredentialsManagerException .Code .NO_NETWORK
636+ else -> CredentialsManagerException .Code .API_ERROR
637+ }
638+ callback.onFailure(
639+ CredentialsManagerException (
640+ exception, error
641+ )
642+ )
643+ }
644+ }
645+
646+ }
647+
499648 /* *
500649 * Checks if a non-expired pair of credentials can be obtained from this manager.
501650 *
@@ -536,6 +685,14 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
536685 storage.remove(LEGACY_KEY_CACHE_EXPIRES_AT )
537686 }
538687
688+ /* *
689+ * Removes the credentials for the given audience from the storage if present.
690+ */
691+ override fun clearApiCredentials (audience : String ) {
692+ storage.remove(audience)
693+ Log .d(TAG , " API Credentials for $audience were just removed from the storage" )
694+ }
695+
539696 /* *
540697 * Helper method to store the given [SSOCredentials] refresh token in the storage.
541698 * Method will silently return if the passed credentials have no refresh token.
0 commit comments