Description
When Salesforce Refresh Token Rotation is enabled on a Connected App, each time a refresh token is used it is replaced with a new one and the old token is immediately invalidated. The Android Mobile SDK does not persist the new refresh token after a successful token refresh, causing all subsequent refresh attempts to fail with an invalid refresh token error, which triggers automatic logout.
Root Cause
In ClientManager.java, the AccMgrAuthTokenProvider.refreshStaleToken() method (v13.2.0) calls UserAccountManager.getInstance().updateAccount(account, updatedUserAccount) after a successful OAuth2.refreshAuthToken() call. However:
updateAccount() only calls accountManager.setUserData() for each key in the bundle built by buildAuthBundle()
buildAuthBundle() does not include the refresh token — it is stored separately as the account password via accountManager.setPassword()
- Therefore, when
TokenEndpointResponse contains a new refresh_token (as it does with Refresh Token Rotation), the new refresh token is never persisted
The next time the SDK attempts to refresh the access token, it reads the old (now-invalidated) refresh token from accountManager.getPassword(), which Salesforce rejects. Since revokedTokenShouldLogout defaults to true, this triggers an automatic logout.
Steps to Reproduce
- Enable Refresh Token Rotation on the Connected App in Salesforce Setup
- Log in to the Android app
- Wait for the access token to expire (or trigger a reauthentication)
- The initial token refresh after login succeeds, rotating the refresh token
- The next token refresh fails because the old refresh token is still stored in
AccountManager.getPassword()
- The SDK logs the user out automatically
Expected Behavior
After a successful token refresh that returns a new refresh_token, the new refresh token should be persisted to AccountManager via setPassword().
Suggested Fix
In ClientManager.AccMgrAuthTokenProvider.refreshStaleToken(), after the updateAccount() call, persist the new refresh token:
UserAccountManager.getInstance().updateAccount(account, updatedUserAccount);
// Persist the rotated refresh token
if (tr.refreshToken != null && !tr.refreshToken.isEmpty()) {
final String encryptionKey = SalesforceSDKManager.getEncryptionKey();
final AccountManager mgr = AccountManager.get(
SalesforceSDKManager.getInstance().getAppContext());
mgr.setPassword(account,
SalesforceSDKManager.encrypt(tr.refreshToken, encryptionKey));
refreshToken = tr.refreshToken;
}
Note: the refreshToken field on AccMgrAuthTokenProvider also needs to be changed from final to non-final to allow the in-memory update.
Alternatively, updateAccount() or buildAuthBundle() could be updated to include the refresh token, but setPassword() is the mechanism used by createAccount() so it would be consistent to use the same approach.
Affected Versions
- Confirmed on 13.2.0 (latest release as of May 2026)
- Likely affects all versions that support Refresh Token Rotation
Environment
- Android SDK 13.2.0 (Maven artifact
com.salesforce.mobilesdk:SalesforceSDK:13.2.0)
- OAuth User-Agent flow (
hybrid_token grant type)
- Salesforce Refresh Token Rotation enabled on Connected App
Description
When Salesforce Refresh Token Rotation is enabled on a Connected App, each time a refresh token is used it is replaced with a new one and the old token is immediately invalidated. The Android Mobile SDK does not persist the new refresh token after a successful token refresh, causing all subsequent refresh attempts to fail with an invalid refresh token error, which triggers automatic logout.
Root Cause
In
ClientManager.java, theAccMgrAuthTokenProvider.refreshStaleToken()method (v13.2.0) callsUserAccountManager.getInstance().updateAccount(account, updatedUserAccount)after a successfulOAuth2.refreshAuthToken()call. However:updateAccount()only callsaccountManager.setUserData()for each key in the bundle built bybuildAuthBundle()buildAuthBundle()does not include the refresh token — it is stored separately as the account password viaaccountManager.setPassword()TokenEndpointResponsecontains a newrefresh_token(as it does with Refresh Token Rotation), the new refresh token is never persistedThe next time the SDK attempts to refresh the access token, it reads the old (now-invalidated) refresh token from
accountManager.getPassword(), which Salesforce rejects. SincerevokedTokenShouldLogoutdefaults totrue, this triggers an automatic logout.Steps to Reproduce
AccountManager.getPassword()Expected Behavior
After a successful token refresh that returns a new
refresh_token, the new refresh token should be persisted toAccountManagerviasetPassword().Suggested Fix
In
ClientManager.AccMgrAuthTokenProvider.refreshStaleToken(), after theupdateAccount()call, persist the new refresh token:Note: the
refreshTokenfield onAccMgrAuthTokenProvideralso needs to be changed fromfinalto non-final to allow the in-memory update.Alternatively,
updateAccount()orbuildAuthBundle()could be updated to include the refresh token, butsetPassword()is the mechanism used bycreateAccount()so it would be consistent to use the same approach.Affected Versions
Environment
com.salesforce.mobilesdk:SalesforceSDK:13.2.0)hybrid_tokengrant type)