-
Notifications
You must be signed in to change notification settings - Fork 156
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
Changes from all commits
188a009
de1bbd2
600eb1e
d3db663
38ccea9
daec7f4
6fd8364
9b1d161
66ddabd
6f5e847
94605c5
b75899a
3320784
aab6508
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | |||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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 | |||||||||||||||||
|
@@ -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. | |||||||||||||||||
* | |||||||||||||||||
|
@@ -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 { | |||||||||||||||||
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. | |||||||||||||||||
* | |||||||||||||||||
|
@@ -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, | |||||||||||||||||
|
@@ -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 warningCode scanning / CodeQL Result of multiplication cast to wider type Warning
Potential overflow in
int multiplication Error loading related location Loading
Copilot AutofixAI 24 days ago To fix the issue, we need to ensure that the multiplication The specific change will be made on line 610, where
Suggested changeset
1
auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt
Copilot is powered by AI and may make mistakes. Always verify output.
Positive FeedbackNegative Feedback
Refresh and try again.
|
|||||||||||||||||
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. | |||||||||||||||||
* | |||||||||||||||||
|
@@ -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) | |||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be logged like in the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | |||||||||||||||||
|
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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