Skip to content

Conversation

@wmathurin
Copy link
Contributor

  • Checking the scope in the token endpoint response to make sure we got refresh_token
  • Refactored onAuthFlowComplete() to make it testable and added tests for it

Checking the scope in the token endpoint response to make sure we got refresh_token (and id)
Throwing an error if it isn't.
Refactored onAuthFlowComplete() to make it testable and added tests for it.
@github-actions
Copy link

github-actions bot commented Oct 2, 2025

1 Warning
⚠️ Big PR, try to keep changes smaller if you can.

Generated by 🚫 Danger

}

@VisibleForTesting
internal suspend fun onAuthFlowComplete(
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 like the original onAuthFlowComplete except with pieces refactored out in private helper methods - which are not called directly but are called through lambda parameters.

.clientId(consumerKey)
.nativeLogin(nativeLogin)
.build()
account.downloadProfilePhoto()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved this one into fun addAccount(account: UserAccount?, context: Context, isTestRun: Boolean, loginServerManager: LoginServerManager)

val scopeParser = ScopeParser(tokenResponse.scope)

// Check that it has the refresh token scope, otherwise call onAuthFlowError
if (!scopeParser.hasRefreshTokenScope()) {
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 check is new to this PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

Is not having the refresh token scope actually an invalid state/error? Do you still get the refresh scope if the policy is set to expire immediately?

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 never allowed it. We would add the refresh_token scope during login if it was not in bootconfig. So login would fail if the connected app did not have it configured.
I need to try and see what happens if the connected app does not have the scope and I don't check it and also what happens if it is set to expire immediately.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@brandonpage
I just tried the following:

  • an ECA that does not have refresh_token and a client app that does not specify scopes in bootconfig (and without the check above) => login works / token end point response does not include a refresh token
  • an ECA with refresh token set to expire immediately => login works / token end point response includes a refresh token (but it will fail to refresh the access token when used)

I'm okay removing the check. We should include those scenarios in our test plan. Especially to make sure that having that null refresh token does not lead to some unwanted behaviors.

}

@Test
fun testOnAuthFlowComplete_blockIntegrationUser_shouldCallError() = runTest {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Tests against the refactored onAuthFlowComplete().

* @return Scope parameter.
* @return Scope parameter string (possibly empty).
*/
public static String computeScopeParameter(String[] scopes) {
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 an important change for this PR.
ScopeParser.computeScopeParameter(scopes) will leave an empty scopes list unchanged.

* @return Scope parameter string (possibly empty).
*/
@JvmStatic
fun computeScopeParameter(scopes: Array<String>?): String {
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 the critical change of this PR.

Copy link
Contributor

@brandonpage brandonpage left a comment

Choose a reason for hiding this comment

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

Looks good! Really appreciate all of the tests and the effort put into making onAuthFlowComplete more testable!

}
}
return false;
return new ScopeParser(scope).hasScope(scopeToCheck);
Copy link
Contributor

Choose a reason for hiding this comment

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

The syntax isn't as clean in Java, but I wonder if ScopeParser could just be a couple String[] extensions?

Edit: There are several functions in ScopeParser so I see its need to be a class.

Comment on lines 105 to 125
onAuthFlowComplete(
tokenResponse,
loginServer,
consumerKey,
onAuthFlowError,
onAuthFlowSuccess,
buildAccountName,
nativeLogin,
SalesforceSDKManager.getInstance().appContext,
SalesforceSDKManager.getInstance().userAccountManager,
blockIntegrationUser,
getRuntimeConfig(SalesforceSDKManager.getInstance().appContext),
::updateLoggingPrefsHelper,
::fetchUserIdentity,
::startMainActivityHelper,
::setAdministratorPreferences,
::addAccountHelper,
::handleScreenLockPolicy,
::handleBiometricAuthPolicy,
::handleDuplicateUserAccount
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Since the original onAuthFlowComplete is internal you could just update the original and add all of the new values with these as the default parameters.

val scopeParser = ScopeParser(tokenResponse.scope)

// Check that it has the refresh token scope, otherwise call onAuthFlowError
if (!scopeParser.hasRefreshTokenScope()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is not having the refresh token scope actually an invalid state/error? Do you still get the refresh scope if the policy is set to expire immediately?

Comment on lines 394 to 397
SalesforceSDKManager.getInstance().appContext.startActivity(Intent(SalesforceSDKManager.getInstance().appContext, SalesforceSDKManager.getInstance().mainActivityClass).apply {
setPackage(SalesforceSDKManager.getInstance().appContext.packageName)
flags = FLAG_ACTIVITY_NEW_TASK
})
Copy link
Contributor

Choose a reason for hiding this comment

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

NIT: Why remove the with(SalesforceSDKManager.getInstance()) block?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can put it back - it was more readable.

const val REFRESH_TOKEN = "refresh_token"
const val ID = "id"
private const val SINGLE_SPACE = " "
private const val EMPTY_STRING = ""
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't dislike this, but I think we typically just use "" in code like how 0 isn't really a magic number. 🤷

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed, I will get rid of it.

A quick test indicated that:
- login with a CA/ECA that does not have refresh scope works but the token end point does not contain a refresh token
- login with a CA/ECA that has a refresh token that expires immediately works also

More testing is needed to make sure that expired or revoked access token is gracefully handled when we don't have a refresh token (and also when we have an expired refresh token).
@wmathurin
Copy link
Contributor Author

The only test failure "LoginActivityTest.viewModelIsUsingFrontDoorBridge_DefaultValue_onCreateWithoutQrCodeLoginIntent" is unrelated.

@wmathurin wmathurin merged commit 7d868cd into forcedotcom:dev Oct 3, 2025
4 of 6 checks passed
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