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

Conversation

pmathew92
Copy link
Contributor
@pmathew92 pmathew92 commented Mar 18, 2025

Changes

This PR moves the Credentials Manager from a single credentials model to a multiple credentials one, supporting:

1 set of app credentials (the existing functionality)
N sets of API-specific credentials
To this end, two new public methods were added to the Credentials Manager:

// And its coroutine counter part
public fun getApiCredentials(
        audience: String,
        scope: String? = null,
        minTtl: Int = 0,
        parameters: Map<String, String> = emptyMap(),
        headers: Map<String, String> = emptyMap(),
        callback: Callback<APICredentials, CredentialsManagerException>
    )
public fun clearApiCredentials(audience: String)

Testing

Please describe how this can be tested by reviewers. Be specific about anything not tested and reasons why. Since this library has unit testing, tests should be added for new functionality and existing tests should complete without errors.

  • This change adds unit test coverage

  • This change adds integration test coverage

  • This change has been tested on the latest version of the platform/language or why not

Checklist

@pmathew92 pmathew92 requested a review from a team as a code owner March 18, 2025 09:01
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 23 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 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 23 days ago

To fix the issue, we need to ensure that the multiplication minTtl * 1000 is performed safely without risking integer overflow. This can be achieved by casting one of the operands to Long before the multiplication. This ensures that the multiplication is performed in the long domain, which has a much larger range than int.

Specifically, we can cast minTtl to Long before multiplying it by 1000. This change is minimal and does not alter the logic of the code. The corrected line will look like this: (minTtl.toLong() * 1000).


Suggested changeset 1
auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.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/SecureCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt
--- a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt
+++ b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt
@@ -942,3 +942,3 @@
                 if (willAccessTokenExpire) {
-                    val tokenLifetime = (expiresAt - currentTimeInMillis - minTtl * 1000) / -1000
+                    val tokenLifetime = (expiresAt - currentTimeInMillis - minTtl.toLong() * 1000) / -1000
                     val wrongTtlException = CredentialsManagerException(
EOF
@@ -942,3 +942,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.
* @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

@@ -305,6 +323,45 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
}
}

/**
* 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 case as they are still valid.
Copy link
Contributor

Choose a reason for hiding this comment

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

This method is an async wrapper, so there is no success case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed

* token is expired. Otherwise, the retrieved API credentials will be returned via the success case 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 stored in storage.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* New or renewed API credentials will be automatically stored in storage.
* New or renewed API credentials will be automatically persisted in 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.

Done

* token is expired. Otherwise, the retrieved API credentials will be returned via the success case 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 stored in storage.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* New or renewed API credentials will be automatically stored in storage.
* New or renewed API credentials will be automatically persisted in 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.

Done


/**
* 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 case as they are still valid.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* token is expired. Otherwise, the retrieved API credentials will be returned via the success case as they are still valid.
* token is expired. Otherwise, the retrieved API credentials will be returned via the success callback as they are still valid.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@@ -406,6 +437,48 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
}
}

/**
* 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 case as they are still valid.
Copy link
Contributor

Choose a reason for hiding this comment

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

This method is an async wrapper, so there is no success case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed

* token is expired. Otherwise, the retrieved API credentials will be returned via the success case 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 stored in storage.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* New or renewed API credentials will be automatically stored in storage.
* New or renewed API credentials will be automatically persisted in 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.

Done

@@ -553,6 +625,47 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
}
}

/**
* 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 case as they are still valid.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* token is expired. Otherwise, the retrieved API credentials will be returned via the success case as they are still valid.
* token is expired. Otherwise, the retrieved API credentials will be returned via the success callback as they are still valid.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

) {
val localAuthenticationManager = localAuthenticationManagerFactory?.create(
activity = fragmentActivity,
authenticationOptions = localAuthenticationOptions!!,
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 value be passed via argument, like fragmentActivity and biometricResultCallback?

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 is a global property that will be set when the instance of the SecureCredentialsManager is created. Hence no need to pass as an argument

Copy link
Contributor

Choose a reason for hiding this comment

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

So is fragmentActivity.

Copy link
Contributor Author
@pmathew92 pmathew92 May 8, 2025

Choose a reason for hiding this comment

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

Yes. but fragment activity is a weak reference which we check if there is a valid reference present or not before invoking the methods. Hence they are passed as arguments

Copy link
Contributor
@Widcket Widcket May 8, 2025

Choose a reason for hiding this comment

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

Yes, but you also check that localAuthenticationOptions is not null before calling this method. Like you do for the fragment activity.

Copy link
Contributor

Choose a reason for hiding this comment

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

The only difference between the two is that one is weak and the other is not, but both are nullable. Hence the comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We do have the check of localAuthenticationOptions != null before calling . Hence we are accessing this globally. For fragment activity we have two null checks , one of the property is not null and they secondary check to ensure the reference inside the property is still non null or not. We pass this non null reference as local parameter ,to avoid any issues even if the main reference got destroyed.

if (fragmentActivity != null && localAuthenticationOptions != null && localAuthenticationManagerFactory != null) {

            fragmentActivity.get()?.let { fragmentActivity ->
                startBiometricAuthentication(
                    fragmentActivity,
                    biometricAuthenticationApiCredentialsCallback(
                        audience, scope, minTtl, parameters, headers, callback
                    )
                )
            } ?: run {
                callback.onFailure(CredentialsManagerException.BIOMETRIC_ERROR_NO_ACTIVITY)
            }
            return
        }
        

* 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

internal fun continueGetApiCredentials(
audience: String,
scope: String?,
minTtl: Int = 0,
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this argument need a default value here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not needed. Removed

@field:SerializedName("expires_at")
val expiresAt: Date,
/**
* Getter for the access token's granted scope. Only available if the requested scope differs from the granted one.
Copy link
Contributor

Choose a reason for hiding this comment

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

Only available if the requested scope differs from the granted one. This doesn't seem to be the case anymore.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed

* Converts a Credentials instance to an APICredentials instance.
*/
internal fun Credentials.toAPICredentials(): APICredentials {
val newScope = scope ?: "openid"
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd suggest using an empty string instead. If the server response doesn't include a scope value, we cannot assume openid was the only requested scope.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

val auth0 = auth0
val client = AuthenticationAPIClient(auth0)
mockAPI.willReturnSuccessfulLogin()
val credentials = client.renewAuth(refreshToken = "refreshToken", scope = "read:data")
Copy link
Contributor

Choose a reason for hiding this comment

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

To differentiate this case from shouldRenewAuthWithOAuthAudienceAndScopeEnforcingOpendId in the "enforcing openid" aspect, I'd suggest using openid read:data as the scope value here.

Copy link
Contributor
@Widcket Widcket May 7, 2025

Choose a reason for hiding this comment

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

Also, that would make shouldRenewAuthWithOAuthAudienceAndScopeNotEnforcingOpendId redundant, so it could be removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

val renewedCredentials =
Credentials("newId", "newAccess", "newType", newRefresh, newDate, "newScope")
Mockito.`when`(request.execute()).thenReturn(renewedCredentials)
manager.getApiCredentials("audience", "newScope", minTtl = 10, callback = apiCredentialsCallback)
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd suggest using the same scope here to make it clear the renewal is not happening because of the different scopes, but because of the minTtl.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

val renewedCredentials =
Credentials("newId", "newAccess", "newType", newRefresh, newDate, "newScope")
Mockito.`when`(request.execute()).thenReturn(renewedCredentials)
manager.getApiCredentials("audience", "newScope", callback = apiCredentialsCallback)
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd suggest using the same scope here to make it clear that the renewal is not happening because of the different scopes but because the access token has expired.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done


// Verify the credentials are property stored
verify(storage).store("com.auth0.id_token", renewedCredentials.idToken)
// RefreshToken should not be replaced
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// RefreshToken should not be replaced
// RefreshToken should be replaced

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed


// Verify the credentials are property stored
verify(storage).store(eq("com.auth0.credentials"), stringCaptor.capture())
MatcherAssert.assertThat(stringCaptor.firstValue, Is.`is`(Matchers.notNullValue()))
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we assert here that the refresh token was not replaced?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

apiCredentialsCaptor.capture()
)

// Verify the returned credentials are the latest
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

apiCredentialsCaptor.capture()
)

// Verify the returned credentials are the latest
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

apiCredentialsCaptor.capture()
)

// Verify the returned credentials are the latest
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines 2427 to 2428

// Verify the returned credentials are the latest
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Verify the returned credentials are the latest

Copy link
Contributor Author

Choose a rea F438 son for hiding this comment

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

Done


// Verify the returned credentials are the latest
val exception = exceptionCaptor.firstValue
MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue()))
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we assert on the exception message here?

Copy link
Contributor Author
@pmathew92 pmathew92 May 8, 2025

Choose a reason for hiding this comment

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

The timing shown in the message can vary dynamically so can't have the text

@pmathew92 pmathew92 merged commit 7f0c68f into main May 8, 2025
7 checks passed
@pmathew92 pmathew92 deleted the SDK-5778 branch May 8, 2025 13:25
@pmathew92 pmathew92 mentioned this pull request May 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants
0