8000 Add support for Multi-Resource Refresh Token (MRRT) by pmathew92 · Pull Request #811 · auth0/Auth0.Android · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Add support for Multi-Resource Refresh Token (MRRT) #811

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
May 8, 2025
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
Expand Up @@ -731,7 +731,9 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
}

/**
* Requests new Credentials using a valid Refresh Token. The received token will have the same audience and scope as first requested.
* Requests new Credentials using a valid Refresh Token. You can request credentials for a specific API by passing its audience value. The default scopes
* configured for the API will be granted if you don't request any specific scopes.
*
*
* This method will use the /oauth/token endpoint with the 'refresh_token' grant, and the response will include an id_token and an access_token if 'openid' scope was requested when the refresh_token was obtained.
* Additionally, if the application has Refresh Token Rotation configured, a new one-time use refresh token will also be included in the response.
Expand All @@ -740,22 +742,35 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
*
* Example usage:
* ```
* client.renewAuth("{refresh_token}")
* .addParameter("scope", "openid profile email")
* client.renewAuth("{refresh_token}","{audience}","{scope})
* .start(object: Callback<Credentials, AuthenticationException> {
* override fun onSuccess(result: Credentials) { }
* override fun onFailure(error: AuthenticationException) { }
* })
* ```
*
* @param refreshToken used to fetch the new Credentials.
* @param audience Identifier of the API that your application is requesting access to. Defaults to null.
* @param scope Space-separated list of scope values to request. Defaults to null.
* @return a request to start
*/
public fun renewAuth(refreshToken: String): Request<Credentials, AuthenticationException> {
public fun renewAuth(
refreshToken: String,
audience: String? = null,
scope: String? = null
): Request<Credentials, AuthenticationException> {
val parameters = ParameterBuilder.newBuilder()
.setClientId(clientId)
.setRefreshToken(refreshToken)
.setGrantType(ParameterBuilder.GRANT_TYPE_REFRESH_TOKEN)
.apply {
audience?.let {
setAudience(it)
}
scope?.let {
setScope(it)
}
}
.asDictionary()
val url = auth0.getDomainUrl().toHttpUrl().newBuilder()
.addPathSegment(OAUTH_PATH)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.auth0.android.authentication.storage
import androidx.annotation.VisibleForTesting
import com.auth0.android.authentication.AuthenticationAPIClient
import com.auth0.android.callback.Callback
import com.auth0.android.result.APICredentials
import com.auth0.android.result.Credentials
import com.auth0.android.result.SSOCredentials
import com.auth0.android.util.Clock
Expand Down Expand Up @@ -30,6 +31,7 @@ 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 getCredentials(callback: Callback<Credentials, CredentialsManagerException>)
public abstract fun getSsoCredentials(
parameters: Map<String, String>,
Expand Down Expand Up @@ -70,6 +72,15 @@ public abstract class BaseCredentialsManager internal constructor(
callback: Callback<Credentials, CredentialsManagerException>
)

public abstract fun getApiCredentials(
audience: String,
scope: String? = null,
minTtl: Int = 0,
parameters: Map<String, String> = emptyMap(),
headers: Map<String, String> = emptyMap(),
callback: Callback<APICredentials, CredentialsManagerException>
)

@JvmSynthetic
@Throws(CredentialsManagerException::class)
public abstract suspend fun awaitSsoCredentials(parameters: Map<String, String>)
Expand Down Expand Up @@ -115,7 +126,18 @@ public abstract class BaseCredentialsManager internal constructor(
forceRefresh: Boolean
): Credentials

@JvmSynthetic
@Throws(CredentialsManagerException::class)
public abstract suspend fun awaitApiCredentials(
audience: String,
scope: String? = null,
minTtl: Int = 0,
parameters: Map<String, String> = emptyMap(),
headers: Map<String, String> = emptyMap()
): APICredentials

public abstract fun clearCredentials()
public abstract fun clearApiCredentials(audience: String)
public abstract fun hasValidCredentials(): Boolean
public abstract fun hasValidCredentials(minTtl: Long): Boolean

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@
import com.auth0.android.authentication.AuthenticationAPIClient
import com.auth0.android.authentication.AuthenticationException
import com.auth0.android.callback.Callback
import com.auth0.android.request.internal.GsonProvider
import com.auth0.android.result.APICredentials
import com.auth0.android.result.Credentials
import com.auth0.android.result.SSOCredentials
import com.auth0.android.result.toAPICredentials
import com.google.gson.Gson
import kotlinx.coroutines.suspendCancellableCoroutine
import java.util.*
import java.util.concurrent.Executor
Expand All @@ -24,6 +28,9 @@
jwtDecoder: JWTDecoder,
private val serialExecutor: Executor
) : BaseCredentialsManager(authenticationClient, storage, jwtDecoder) {

private val gson: Gson = GsonProvider.gson

/**
* Creates a new instance of the manager that will store the credentials in the given Storage.
*
Expand Down Expand Up @@ -55,6 +62,17 @@
storage.store(LEGACY_KEY_CACHE_EXPIRES_AT, credentials.expiresAt.time)
}

/**
* 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
*/
override fun saveApiCredentials(apiCredentials: APICredentials, audience: String) {
gson.toJson(apiCredentials).let {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this method throw an error if the serialization fails?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't cause any serialization issue and wouldn't require to throw an exception

storage.store(audience, it)
}
}

/**
* Creates a new request to exchange a refresh token for a session transfer token that can be used to perform web single sign-on.
*
Expand Down Expand Up @@ -305,6 +323,44 @@
}
}

/**
* Retrieves API credentials from storage and automatically renews them using the refresh token if the access
* token is expired. Otherwise, the retrieved API credentials will be returned as they are still valid.
*
* If there are no stored API credentials, the refresh token will be exchanged for a new set of API credentials.
* New or renewed API credentials will be automatically persisted in storage.
*
* @param audience Identifier of the API that your application is requesting access to.
* @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 headers additional headers to send in the request to refresh expired credentials.
*/
@JvmSynthetic
@Throws(CredentialsManagerException::class)
override suspend fun awaitApiCredentials(
audience: String,
scope: String?,
minTtl: Int,
parameters: Map<String, String>,
headers: Map<String, String>
): APICredentials {
return suspendCancellableCoroutine { continuation ->
getApiCredentials(
audience, scope, minTtl, parameters, headers,
object : Callback<APICredentials, CredentialsManagerException> {
override fun onSuccess(result: APICredentials) {
continuation.resume(result)
}

override fun onFailure(error: CredentialsManagerException) {
continuation.resumeWithException(error)
}
}
)
}
}

/**
* 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,
Expand Down Expand Up @@ -496,6 +552,99 @@
}
}


/**
* Retrieves API credentials from storage and automatically renews them using the refresh token if the access
* token is expired. Otherwise, the retrieved API credentials will be returned via the success callback as they are still valid.
*
* If there are no stored API credentials, the refresh token will be exchanged for a new set of API credentials.
* New or renewed API credentials will be automatically persisted in storage.
*
* @param audience Identifier of the API that your application is requesting access to.
* @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 headers headers to use when exchanging a refresh token for API credentials.
* @param callback the callback that will receive a valid [Credentials] or the [CredentialsManagerException].
*/
override fun getApiCredentials(
audience: String,
scope: String?,
minTtl: Int,
parameters: Map<String, String>,
headers: Map<String, String>,
callback: Callback<APICredentials, CredentialsManagerException>
) {
serialExecutor.execute {
//Check if existing api credentials are present and valid
val apiCredentialsJson = storage.retrieveString(audience)
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 hasExpired = hasExpired(apiCredentials.expiresAt.time)
if (!hasExpired && !willTokenExpire && !scopeChanged) {
callback.onSuccess(apiCredentials)
return@execute
}
}
//Check if refresh token exists or not
val refreshToken = storage.retrieveString(KEY_REFRESH_TOKEN)
if (refreshToken == null) {
callback.onFailure(CredentialsManagerException.NO_REFRESH_TOKEN)
return@execute
}

val request = authenticationClient.renewAuth(refreshToken, audience, scope)
request.addParameters(parameters)

for (header in headers) {
request.addHeader(header.key, header.value)
}

try {
val newCredentials = request.execute()
val expiresAt = newCredentials.expiresAt.time
val willAccessTokenExpire = willExpire(expiresAt, minTtl.toLong())
if (willAccessTokenExpire) {
val tokenLifetime = (expiresAt - currentTimeInMillis - minTtl * 1000) / -1000

Check warning

Code scanning / CodeQL

Result of multiplication cast to wider type Warning

Potential overflow in
int multiplication
before it is converted to long by use in a numeric context.

Copilot Autofix

AI 24 days ago

To fix the issue, we need to ensure that the multiplication minTtl * 1000 is performed in a long context to avoid integer overflow. This can be achieved by explicitly casting one of the operands (minTtl or 1000) to long before the multiplication. This ensures that the result of the multiplication is a long and can safely handle larger values.

The specific change will be made on line 610, where minTtl * 1000 is replaced with minTtl.toLong() * 1000. This change does not alter the logic of the program but ensures that the multiplication is performed in a long context.


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

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt
--- a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt
+++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt
@@ -609,3 +609,3 @@
                 if (willAccessTokenExpire) {
-                    val tokenLifetime = (expiresAt - currentTimeInMillis - minTtl * 1000) / -1000
+                    val tokenLifetime = (expiresAt - currentTimeInMillis - minTtl.toLong() * 1000) / -1000
                     val wrongTtlException = CredentialsManagerException(
EOF
@@ -609,3 +609,3 @@
if (willAccessTokenExpire) {
val tokenLifetime = (expiresAt - currentTimeInMillis - minTtl * 1000) / -1000
val tokenLifetime = (expiresAt - currentTimeInMillis - minTtl.toLong() * 1000) / -1000
val wrongTtlException = CredentialsManagerException(
Copilot is powered by AI and may make mistakes. Always verify output.
val wrongTtlException = CredentialsManagerException(
CredentialsManagerException.Code.LARGE_MIN_TTL, String.format(
Locale.getDefault(),
"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.",
tokenLifetime,
minTtl
)
)
callback.onFailure(wrongTtlException)
return@execute
}

// non-empty refresh token for refresh token rotation scenarios
val updatedRefreshToken =
if (TextUtils.isEmpty(newCredentials.refreshToken)) refreshToken else newCredentials.refreshToken
val newApiCredentials = newCredentials.toAPICredentials()
storage.store(KEY_REFRESH_TOKEN, updatedRefreshToken)
storage.store(KEY_ID_TOKEN, newCredentials.idToken)
saveApiCredentials(newApiCredentials, audience)
callback.onSuccess(newApiCredentials)
} catch (error: AuthenticationException) {
val exception = when {
error.isRefreshTokenDeleted || error.isInvalidRefreshToken -> CredentialsManagerException.Code.RENEW_FAILED

error.isNetworkError -> CredentialsManagerException.Code.NO_NETWORK
else -> CredentialsManagerException.Code.API_ERROR
}
callback.onFailure(
CredentialsManagerException(
exception, error
)
)
}
}

}

/**
* Checks if a non-expired pair of credentials can be obtained from this manager.
*
Expand Down Expand Up @@ -536,6 +685,14 @@
storage.remove(LEGACY_KEY_CACHE_EXPIRES_AT)
}

/**
* Removes the credentials for the given audience from the storage if present.
*/
override fun clearApiCredentials(audience: String) {
storage.remove(audience)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be logged like in the SecureCredentialsManager? E.g.:
Log.d(TAG, "API Credentials for $audience were just removed from the storage")

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

Log.d(TAG, "API Credentials for $audience were just removed from the storage")
}

/**
* Helper method to store the given [SSOCredentials] refresh token in the storage.
* Method will silently return if the passed credentials have no refresh token.
Expand Down
Loading
0